{"id":19063237,"url":"https://github.com/delner/olfactory","last_synced_at":"2026-06-20T19:31:21.192Z","repository":{"id":24488945,"uuid":"27893575","full_name":"delner/olfactory","owner":"delner","description":"A factory template engine to instantiate complex objects.","archived":false,"fork":false,"pushed_at":"2016-05-22T00:11:13.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-12-04T14:44:16.530Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/delner.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":"2014-12-11T22:00:09.000Z","updated_at":"2018-03-31T13:54:27.000Z","dependencies_parsed_at":"2022-09-14T01:02:20.823Z","dependency_job_id":null,"html_url":"https://github.com/delner/olfactory","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/delner/olfactory","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delner%2Folfactory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delner%2Folfactory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delner%2Folfactory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delner%2Folfactory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/delner","download_url":"https://codeload.github.com/delner/olfactory/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/delner%2Folfactory/sbom","scorecard":{"id":333194,"data":{"date":"2025-08-11","repo":{"name":"github.com/delner/olfactory","commit":"ee79c45197a3e7b1abc47332a664e33c5c72208c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"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":-1,"reason":"no dependencies found","details":null,"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":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"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":"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":"Dangerous-Workflow","score":-1,"reason":"no workflows found","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":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"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":"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":"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":"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":"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 'master'"],"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"}}]},"last_synced_at":"2025-08-18T04:04:26.189Z","repository_id":24488945,"created_at":"2025-08-18T04:04:26.189Z","updated_at":"2025-08-18T04:04:26.189Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34583589,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"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":[],"created_at":"2024-11-09T00:29:21.327Z","updated_at":"2026-06-20T19:31:21.170Z","avatar_url":"https://github.com/delner.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"Olfactory\n==========\n\n[![Build Status](https://travis-ci.org/delner/olfactory.svg?branch=master)](https://travis-ci.org/delner/olfactory) ![Gem Version](https://img.shields.io/gem/v/olfactory.svg?maxAge=2592000)\n###### *For Ruby 2+*\n\n### Introduction\n\nOlfactory is a factory extension for creating complex object sets, as a supplement to `factory_girl`, `fabrication`, or other factories.\n\nIt introduces the concept of **templates**: a group of named values/objects (as a `Hash`.) You define what objects (or other templates) your template can contain (similar to a factory), then you can create instances of that template using the `#build` or `#create` functions. These templates can be used to make test setup much quicker and easier. Templates are not intended to replace factories, but bridge the gap where `factory_girl` and `fabrication` factories fall short.\n\nThey are most useful when:\n - Your models are weakly related or non-relational (e.g. not joined by ActiveRecord associations)\n - You require a collection of objects that are not directly related (or easily built using factories \u0026 associations)\n - You'd prefer short-hand notation\n - You'd like to abstract Factory implementation out of your tests\n   - e.g. Define a set of generic factories other programmers can use, without requiring them to understand the underlying class or relational structure.\n\nFor example, given:\n```ruby\ncontext \"networkable people\" do\n  let(:phone_user) { create(:person, :devices =\u003e [phone]) }\n  let(:phone) { create(:android, apps =\u003e [facebook_app_one, twitter_app_one]) } # Factory(:device)\n  let(:facebook_phone) { create(:facebook, :platform =\u003e :phone) } # Factory(:app)\n  let(:twitter_phone) { create(:twitter, :platform =\u003e :phone) } # Factory(:app)\n  \n  let(:tablet_user) { create(:person, :devices =\u003e [tablet]) }\n  let(:tablet) { create(:tablet, apps =\u003e [facebook_tablet]) }  # Factory(:device)\n  let(:facebook_tablet) { create(:facebook, :platform =\u003e :tablet) }  # Factory(:app)\n  \n  let(:desktop_user) { create(:person, :devices =\u003e [desktop]) }\n  let(:desktop) { create(:desktop, apps =\u003e [twitter_desktop]) }  # Factory(:device)\n  let(:twitter_desktop) { create(:twitter, :platform =\u003e :desktop) }  # Factory(:app)\n  \n  it { expect(phone_user.can_network_with(tablet_user, desktop_user).to be_true }\nend\n```\n\nThis is a lot of setup for a simple test case. We could write a bunch of named factories, but then the test logic simply ends up in a factory file rather than our test; not good.\n\nInstead, we can use templates to define shorthand notation:\n```ruby\ncontext \"networkable people\" do\n  let(:user_group) do\n    Olfactory.create :user_group do |group|\n      group.user :desktop_user { |user| user.phone { |phone| phone.apps :facebook, :twitter } }\n      group.user :tablet_user { |user| user.tablet { |tablet| tablet.app :facebook } }\n      group.user :phone_user { |user| user.desktop { |desktop| desktop.app :twitter } }\n    end\n  end\n  subject { user_group[:users] }\n  it { expect(subject[:desktop_user].can_network_with(subject[:tablet_user], subject[:phone_user]).to be_true }\nend\n```\n\nIn this sample, the `let(:user_group)` block returns a `Hash` object, that contains structured, pre-fabricated data of our choosing that we can use for our `it` example.\n\n### Usage\n\n##### What is a template?\n\nTemplates are effectively `Hash` schemas. By defining a template, you are specifying which named-values can appear in a `Hash` instance of that template. When defining a template, you can also define custom presets, sequences, and other named options. You can leverage these features to simplify how you create complex objects \u0026 test data.\n\n##### Defining templates\n\nTemplates are defined in `spec/templates/**/*.rb` files. Define a template using the `Olfactory#template` method.\n\n    Olfactory.template :computer do |t|\n      ...\n    end\n\nOnce defined, these templates can be instantiated using `build` and `create`, which are analogous to the same `factory_girl`/`fabrication` methods\n\n    Olfactory.build :computer # Creates objects, but does not implictly save them\n    Olfactory.create :computer # Creates objects, and attempts to save all items in the Hash that respond to #save!\n\nInvoking these two methods will return a `Hash` matching the template schema, populated with either custom or preset values.\n\n##### Defining template relationships using `has` \u0026 `embeds`\n\nEvery template is composed of two kinds of named values: *fields* or other *templates*.\n\nFields hold actual values: integers, strings, objects, etc. The `has` relation is used define a field. `#has_one` holds one object, and '#has_many' holds a collection of objects (`Array` or `Hash`.)\n\nYou can also embed a template within another template. This is useful if you have a template composed of other smaller sub-templates (e.g. a Computer template composed of Processor and Memory templates.) Use the `embeds` relation to nest a template. `#embeds_one` will embed a single instance of a template. `#embeds_many` will embed a collection of template instances (in the form of an `Array` or `Hash`.)\n\n##### #has_one\n\nDefines a field containing a single object.\n\nDefinition:\n\u003e **has_one** :name *[, :alias =\u003e alias_name]*\n\n- `:alias` defines an alias keyword for setting the item value.\n\nWhen using:\n\u003e **name|alias_name** object|\u0026block\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      t.has_one :keyboard\n      t.has_one :mouse\n      t.has_one :cpu, :alias =\u003e :processor\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      c.keyboard \"X4 Sidewinder\"\n      c.mouse { FactoryGirl::build(:mouse) }\n      c.processor \"Intel Xeon\"\n    end\n    # Result\n    {\n      :keyboard =\u003e \"X4 Sidewinder\",\n      :mouse =\u003e \u003cMouse\u003e,\n      :cpu =\u003e \"Intel Xeon\"\n    }\n\n##### #has_many\n\nDefines a collection of objects. Each invocation appends the resulting items to the collection. Collection is `nil` by default (if no items are added.)\n\nDefinition:\n\u003e **has_many** :name *[, :alias =\u003e alias_name,\n\u003e                   :singular =\u003e singular_name,\n\u003e                   :named =\u003e true|false]*\n\n- `:alias` defines an alias keyword for adding mutiple items to the collection.\n- `:singular` defines the keyword for adding singular items to the collection.\n- `:named` converts the collection to a `Hash`. When true, all invocations must provide a name (first argument.)\n\nWhen using as generic collection (`named == false`):\n\u003e Singular:\n\u003e\n\u003e **singular_name** Object\n\u003e \n\u003e **singular_name** { \u0026block }\n\u003e\n\u003e Plural:\n\u003e \n\u003e **name|alias_name** quantity:Integer { \u0026block }\n\u003e \n\u003e **name|alias_name** objects:Array\n\u003e \n\u003e **name|alias_name** object1, object2...\n\nWhen using as a named collection(`named == true`):\n\u003e Singular:\n\u003e\n\u003e **singular_name** variable_name, Object\n\u003e \n\u003e **singular_name** variable_name { \u0026block }\n\u003e\n\u003e Plural:\n\u003e \n\u003e **name|alias_name** Hash\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      # Generic\n      t.has_many :cpus\n      t.has_many :memory_sticks, :singular =\u003e :memory_stick\n      \n      t.has_many :usb_ports\n      \n      # Named\n      t.has_many :drives, :singular =\u003e :drive, :named =\u003e true\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      # Generic\n      c.cpus \"Intel i7\", \"Onboard graphics\"\n      c.memory_stick \"2GB\"\n      c.memory_stick \"2GB\"\n      c.usb_ports 2 do\n        \"2.0\"\n      end\n      c.usb_ports [\"2.0\", \"2.0\"]\n      c.usb_ports \"3.0\", \"3.0\"\n      \n      # Named\n      c.drive :ssd \"Samsung\"\n      c.drives { :hdd =\u003e \"Seagate\", :optical =\u003e \"Memorex\" }\n    end\n    # Result\n    {\n      :cpus =\u003e [\"Intel i7\", \"Onboard graphics\"],\n      :memory_sticks =\u003e [\"2GB\", \"2GB\"],\n      :drives =\u003e {\n        :ssd =\u003e \"Samsung\",\n        :hdd =\u003e \"Seagate\",\n        :optical =\u003e \"Memorex\"\n      },\n      :usb_ports =\u003e [\"2.0\", \"2.0\", \"2.0\", \"2.0\", \"3.0\", \"3.0\"]\n    }\n    \n##### #embeds_one\n\nDefines a field containing an embedded template.\n\nDefinition:\n\u003e **embeds_one** :name *[, :alias =\u003e alias_name, :template =\u003e template_name]*\n\n- `:alias` defines an alias keyword for setting the embedded template value.\n- `:template` defines the actual name of the template used, if it does not match the name.\n\nWhen using:\n\u003e **name|alias_name** *[preset_name, { \u0026block }]*\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      t.embeds_one :cpu\n      t.embeds_one :gpu, :template =\u003e :cpu\n    end\n    Olfactory.template :cpu do |t|\n      t.has_many :cores\n      preset :amd do |p|\n        p.cores \"AMD Core\", \"AMD Core\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :computer do |computer|\n      computer.cpu do |cpu|\n        cpu.cores \"Intel Core\", \"Intel Core\"\n      end\n      computer.gpu :amd\n    end\n    # Result\n    {\n      :cpu =\u003e {\n                :cpu_core =\u003e [\"Intel Core\", \"Intel Core\"]\n              },\n      :gpu =\u003e {\n                :gpu_core =\u003e [\"AMD Core\", \"AMD Core\"]\n              }\n    }\n    \n##### #embeds_many\n\nDefines a collection of templates. Each invocation appends the resulting templates to the collection. Collection is `nil` by default (if no templates are added.)\n\nDefinition:\n\u003e **has_many** :name *[, :alias =\u003e alias_name,\n\u003e                   :template =\u003e template_name,\n\u003e                   :singular =\u003e singular_name,\n\u003e                   :named =\u003e true|false]*\n\n- `:alias` defines an alias keyword for adding mutiple templates to the collection.\n- `:template` defines the actual name of the template used, if it does not match the name.\n- `:singular` defines the keyword for adding singular templates to the collection.\n- `:named` converts the collection to a `Hash`. When true, all invocations must provide a name (first argument.)\n\nWhen using as generic collection (`named == false`):\n\u003e Singular:\n\u003e\n\u003e **singular_name**\n\u003e \n\u003e **singular_name** preset_name\n\u003e\n\u003e **singular_name** { \u0026block }\n\u003e\n\u003e Plural:\n\u003e \n\u003e **name|alias_name** quantity:Integer\n\u003e \n\u003e **name|alias_name** preset_name, quantity:Integer\n\u003e \n\u003e **name|alias_name** quantity:Integer, preset_name\n\u003e \n\u003e **name|alias_name** quantity:Integer { \u0026block }\n\nWhen using as a named collection(`named == true`):\n\u003e Singular:\n\u003e \n\u003e **singular_name** variable_name\n\u003e\n\u003e **singular_name** variable_name, preset_name\n\u003e \n\u003e **singular_name** variable_name { \u0026block }\n\u003e\n\u003e Plural:\n\u003e \n\u003e (None)\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      # Generic\n      t.embeds_many :cpus, :singular =\u003e :cpu\n      t.embeds_many :drives, :singular =\u003e :drive, :named =\u003e true\n    end\n    Olfactory.template :cpu do |t|\n      # Generic\n      t.has_many :cores, :singular =\u003e :core\n      t.preset :amd do |p|\n        p.cores \"AMD Core\", \"AMD Core\"\n      end\n    end\n    Olfactory.template :drive do |t|\n      # Generic\n      t.has_one :storage_size\n      t.has_one :manufacturer\n      t.preset :samsung_512gb do |p|\n        p.storage_size 512000\n        p.manufacturer \"Samsung\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      # Generic\n      computer.cpu :amd\n      computer.cpu do |cpu|\n        cpu.cores \"Intel Core\", \"Intel Core\"\n      end\n      computer.cpus 2 # Creates 2 :cpu templates with defaults\n      computer.cpus :amd, 2\n      computer.cpus 2, :amd\n      computer.cpus 2 do |cpu|\n        cpu.cores \"Intel Core\", \"Intel Core\"\n      end\n      \n      # Named\n      computer.drive :hdd # Creates :drive template with defaults\n      computer.drive :ssd, :samsung_512gb\n      computer.drive :optical do |drive|\n        drive.manufacturer \"Memorex\"\n      end\n    end\n\n##### #sequence\n\nDefines a sequence, similar to `factory_girl`'s sequences. Can be combined with `Faker` to provide auto-generated test data. They can be defined at the global level, or nested within a template.\n\n###### When defining at the global level\n\nDefinition:\n\u003e **Olfactory.sequence** :name*[, :seed =\u003e Integer]* { |iterator[,options]| \u0026block }\n\n- `seed` defines the starting value, which increments by 1 after each invocation. Default 0.\n- `options` in the form of hash args can be passed to the block.\n- `iterator` is the current seed, which starts at 0 by default.\n- `block` generates and returns the value.\n\nUsage:\n\u003e **Olfactory.generate** :name\n\u003e \n\u003e **Olfactory.generate** :name, :option1 =\u003e Object, :option2 =\u003e Object...\n\u003e \n\u003e **Olfactory.generate** :name, :seed =\u003e Integer\n\u003e \n\u003e **Olfactory.generate** :name, :seed =\u003e Integer, :option1 =\u003e Object, :option2 =\u003e Object...\n\u003e \n\u003e **Olfactory.generate** :name { |iterator, options| \u0026block }\n\n - `seed` can be provided to override whatever the current seed is. When you provide an overriding seed, the sequence will *not* increment its internal seed. (Will act like it was never called.)\n - `options` in the form of hash args can be passed to the block.\n - `block` can be provided to override the block used for the call. When you provide an overriding block, the sequence will still increment its internal seed.\n\nSample:\n\n    Olfactory.sequence :ip_address, :seed =\u003e 1 do |n, options|\n      options[:ipv6] ? \"fe80::2a18:78ff:feba:#{n}\" : \"192.168.1.#{n % 255}\"\n    end\n    Olfactory.generate(:ip_address) # =\u003e \"192.168.1.1\"\n    Olfactory.generate(:ip_address, :seed =\u003e 255) # =\u003e \"192.168.1.255\"\n    Olfactory.generate(:ip_address) # =\u003e \"192.168.1.2\"\n    Olfactory.generate(:ip_address, :ipv6 =\u003e true) # =\u003e \"fe80::2a18:78ff:feba:3\"\n    Olfactory.generate(:ip_address) do |n|\n      \"172.168.1.#{n % 255}\"\n    end # =\u003e \"172.168.1.4\"\n    \n###### When defining at the template level\n\n`Olfactory` sequences don't provide much benefit over other gems at the global level, but they really shine when used within templates. Sequences work exactly the same within templates, except they can be bound by `scope`, allowing the author a great deal of control over when sequences reset.\n\n`scope` can either be `:instance`, which resets the seed for each instance of the template, or `:template`, which shares the seed across all instances of the template.\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      t.has_one :serial_number\n      t.sequence :serial_number, :scope =\u003e :template do |n|\n        (10000 + n)\n      end\n      t.has_many :registers, :singular =\u003e :register\n      t.sequence :register, :scope =\u003e :instance do |n|\n        \"Register #{n+1}\"\n      end\n    end\n    \n    Olfactory.build :computer do |c|\n      c.serial_number { c.generate(:serial_number) }\n      c.registers 2 { c.generate(:register) }\n    end\n    # =\u003e { :serial_number =\u003e 10000, :registers =\u003e [\"Register 1\", \"Register 2\"] }\n    \n    Olfactory.build :computer do |c|\n      c.serial_number { c.generate(:serial_number) }\n      c.registers 2 { c.generate(:register) }\n    end\n    # =\u003e { :serial_number =\u003e 10001, :registers =\u003e [\"Register 1\", \"Register 2\"] }\n\n##### #dictionary\n\nDefines a dictionary, which is just a simple data store (Hash.) They can be defined at the global level, or nested within a template.\n\n###### When defining at the global level\n\nDefinition:\n\u003e **Olfactory.dictionary** :name\n\nUsage:\n\u003e **Olfactory.dictionaries**[name] # Returns Hash to read/write from\n\nSample:\n\n    Olfactory.dictionary :manfacturer_codes\n    Olfactory.dictionaries[:manfacturer_codes][\"DELL\"] = \"Dell Computing\"\n\n###### When defining at the template level\n\nA hash data-store isn't that special at the global level, but is much more useful within templates. Definition is the same, but the usage only differs by name. Like sequences, dictionaries can define `scope` to separate or share data across templates. Combining them with sequences, we can synchronize data in some really cool ways.\n\n`scope` can either be `:instance`, which resets the hash for each instance of the template, or `:template`, which shares the hash across all instances of the template.\n\nSample:\n\n    Olfactory.template :computer do |t|\n      t.has_one :hdd_manfacturer_code\n      t.has_one :gpu_manfacturer_code\n      t.has_one :cpu_manfacturer_code\n      t.dictionary :manfacturer_codes, :scope =\u003e :template\n      t.sequence :manfacturer_code, :scope =\u003e :template do |n|\n        (10000 + n)\n      end\n    end\n    \n    Olfactory.build :computer do |c|\n      c.hdd_manfacturer_code { c.manfacturer_codes[\"SAMSUNG\"] ||= c.generate(:manfacturer_code) }\n      c.cpu_manfacturer_code { c.manfacturer_codes[\"AMD\"] ||= c.generate(:manfacturer_code) }\n      c.gpu_manfacturer_code { c.manfacturer_codes[\"AMD\"] ||= c.generate(:manfacturer_code) }\n    end\n    # =\u003e { :hdd_manfacturer_code =\u003e 10001,\n    #      :cpu_manfacturer_code =\u003e 10002,\n    #      :cpu_manfacturer_code =\u003e 10002 }\n    \n    Olfactory.build :computer do |c|\n      c.hdd_manfacturer_code { c.manfacturer_codes[\"SEAGATE\"] ||= c.generate(:manfacturer_code) }\n      c.cpu_manfacturer_code { c.manfacturer_codes[\"AMD\"] ||= c.generate(:manfacturer_code) }\n      c.gpu_manfacturer_code { c.manfacturer_codes[\"INTEL\"] ||= c.generate(:manfacturer_code) }\n    end\n    # =\u003e { :hdd_manfacturer_code =\u003e 10003,\n    #      :cpu_manfacturer_code =\u003e 10002,\n    #      :cpu_manfacturer_code =\u003e 10004 }\n    \n##### #preset\n\nDefines a preset of values.\n\nDefinition:\n\u003e **preset** :name { |instance| \u0026block }\n\nWhen using:\n\u003e Olfactory.build template_name, :preset =\u003e preset_name, :quantity =\u003e quantity:Integer\n\nSee above sections for usage within templates.\n\nSample:\n\n    # Template definition\n    Olfactory.template :computer do |t|\n      t.embeds_many :cpus\n      t.preset :dual_core do |p|\n        p.cpus 2 do |cpu|\n          cpu.core \"Intel Core\"\n        end\n      end\n    end\n    Olfactory.template :cpu do |t|\n      t.has_one :cpu_core\n    end\n    # Build instance of template\n    Olfactory.build :computer, :preset =\u003e :dual_core\n    # Result\n    {\n      :cpus =\u003e [{\n                :cpu_core =\u003e \"Intel Core\"\n              },\n              {\n                :cpu_core =\u003e \"Intel Core\"\n              }]\n    }\n    \n##### #macro\n\nSimilar to a preset, `macro` defines a 'function' that can be used within a template, which can accept parameters, and set variables or invoke code. The result of this macro will *not* be stored on the resulting template. \n\nDefinition:\n\u003e **macro** :name { |instance, *args..| \u0026block }\n\nWhen using:\n\u003e **name** arg1, arg2...\n\nSample:\n\n    # Template defintion\n    Olfactory.template :computer do |t|\n      t.has_one :memory_size\n      t.macro :memory_sticks do |d, num|\n        d.memory_size \"{num*4}GB\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      c.memory_sticks 2\n    end\n    # Result \n    {\n      :memory_size =\u003e \"8GB\"\n    }\n\n##### #transient\n \nSimilar to `factory_girl`'s transients, `transient` defines a temporary variable. You can store values in here to compose conditional logic or more sophisticated templates. When a template contains an embedded template, it will pass down all of its transients to the embedded template. Invoking `transients` on an instance of a template will return a `Hash` of its transient variables.\n\nUsage:\n\u003e **transient** name, Object # Sets value\n\u003e \n\u003e **transient** name { Object } # Sets value (lazily)\n\u003e \n\u003e **transients**[name] # Gets value\n\nSample:\n\n    # Template defintion\n    Olfactory.template :computer do |t|\n      t.has_one :memory_size\n      t.embeds_one :cpu\n      t.macro :memory_sticks do |d, num|\n        d.memory_size \"{num*(d.transients[:memory_stick_size] || 4)}GB\"\n      end\n      t.macro :memory_stick_size do |m, size|\n        t.transient :memory_stick_size, size\n      end\n    end\n    Olfactory.template :cpu do |t|\n      t.has_one :available_memory\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      c.memory_stick_size 2\n      c.memory_sticks 2\n      c.cpu do |cpu|\n        cpu.memory_module_size \"#{cpu.transients[:memory_stick_size]}GB\"\n      end\n    end\n    # Result\n    {\n      :memory_size =\u003e \"4GB\",\n      :cpu =\u003e {\n                :memory_module_size =\u003e \"2GB\"\n              }\n    }\n\n##### Defaults: #before \u0026 #after\n\nDefines default values, which are used to fill in any empty `has`, `embeds` or `transient` fields, before and after respectively. They will *not* overwrite any non-nil value.\n\nDefinition:\n\u003e **before**(*:context, :run =\u003e Symbol*) { |instance| \u0026block }\n\u003e \n\u003e **after** { |instance| \u0026block }\n\n- `:context` defines when this before should run. Specifying `:embedded` means it runs just before embedded objects are added to the instance. Default (by providing no value) is to run immediately as instance is created.\n- `:run` defines how many times this before can be invoked for an instance. Specifying `:once` means it can only be invoked once (singleton-style.) Default is to always execute. Can only be specified if `:context` is also specified.\n\nThe latter two options can be useful if you are embedding a template that reads the parent's fields or transients.\n\nSample:\n\n    # Template defintion\n    Olfactory.template :computer do |t|\n      t.has_one :cpu\n      t.has_one :memory_size\n      t.before do |d|\n        d.cpu \"Intel Xeon\"\n        d.memory_size \"4GB\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :computer do |c|\n      c.cpu \"AMD Athlon\"\n    end\n    # Result \n    {\n      :cpu =\u003e \"AMD Athlon\",\n      :memory_size =\u003e \"4GB\"\n    }\n    \n    # Template defintion\n    Olfactory.template :phone do |t|\n      t.has_one :cpu\n      t.has_one :memory_size\n      t.after do |d|\n        d.cpu \"ARM\"\n        d.memory_size \"2GB\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :phone do |c|\n      c.memory_size \"1GB\"\n    end\n    # Result \n    {\n      :memory_size =\u003e \"1GB\",\n      :cpu =\u003e \"ARM\"\n    }\n\nAnother `before` sample using `:context` and `:run` options:\n\n    # Template defintions\n    Olfactory.template :widget do |t|\n      t.embeds_many :doodads, :singular =\u003e :doodad\n      t.has_one :thingamabob\n      t.macro :quality do |m, type|\n        m.transient :attribute, type.to_s\n      end\n      t.before(:embedded) do |d|\n        d.quality :dull\n        d.thingamabob \"thingamabob\" if d[:doodads] \u0026\u0026 d[:doodads].count \u003e 0\n      end\n    end\n    Olfactory.template :doodad do |t|\n      t.has_one :gizmo\n      t.after do |d|\n        d.gizmo \"#{d.transients[:attribute]} doodad\"\n      end\n    end\n    # Build instance of template\n    Olfactory.build :widget do |w|\n      w.doodad\n      w.quality :shiny\n      w.doodad\n    end\n    # Result\n    {\n      :doodads =\u003e [{ :gizmo =\u003e \"dull doodad\" },\n                   { :gizmo =\u003e \"shiny doodad\" }]\n      # NOTE: A 'thingamabob' wasn't added. This is because the #before only ran once.\n    }\n\n##### #instantiate\n\nDefines an 'instantiator': a function you can call to build custom objects from an instance of a template. The block can accept arguments or use the template instance as input, and should return an object or collection. Invoke the block using the `#build` or `#create` method to get the return value from the instantiator. When `#create` is used, any object that responds to `#save!` will be saved.\n\nDefinition:\n\u003e **instantiate** :name { |instance, *args..| \u0026block }\n\nWhen using:\n\u003e **build**(name*[, arg1, arg2...]*)\n\u003e \n\u003e **create**(name*[, arg1, arg2...]*)\n\nSample:\n\n    # Template defintion\n    Olfactory.template :widget do |t|\n      t.has_one :doodad\n      t.instantiate :doodad do |i, j|\n        String.new(\"#{i[:doodad]}-instance-#{j}\")\n      end\n    end\n    # Build instance of template\n    instance = Olfactory.build :widget do |w| w.doodad \"doodad\" end\n    instance.build(:doodad, 1)\n    # Result\n    \"doodad-instance-1\"\n\n### Changelog\n\n#### Version 0.2.1\n\n - Added: `context` and `run` options to `before` blocks.\n - Added: `dimension` option to sequences, to allow scoping.\n - Fixed: Default values not being overridden in special cases.\n - Fixed: Defaults adding to item and subtemplate collections.\n\n#### Version 0.2.0\n\n - Added: Sequences (like factory_girl's)\n - Added: Dictionaries (generic hash storage)\n - Changed: `#build_template` and `#create_template` have been renamed to `#build` and `#create` respectively.\n\n#### Version 0.1.0\n\n - Initial version of Olfactory (templates, transients, macros, presets, has/embeds relations)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdelner%2Folfactory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdelner%2Folfactory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdelner%2Folfactory/lists"}