{"id":17441462,"url":"https://github.com/farlee2121/fsspec","last_synced_at":"2025-04-15T12:14:57.093Z","repository":{"id":37496088,"uuid":"398088147","full_name":"farlee2121/FsSpec","owner":"farlee2121","description":"FsSpec represents value constraints as data to reuse one constraint declaration for validation, data generation, error explanation, and more.","archived":false,"fork":false,"pushed_at":"2025-02-21T00:43:20.000Z","size":350,"stargazers_count":25,"open_issues_count":3,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-15T12:14:48.921Z","etag":null,"topics":["data-generation","type-driven-development","validation"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/farlee2121.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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-19T22:18:55.000Z","updated_at":"2024-07-12T21:17:40.000Z","dependencies_parsed_at":"2024-02-22T01:43:33.295Z","dependency_job_id":"7b40ef49-6b1e-44f4-853d-adc53a2b07ac","html_url":"https://github.com/farlee2121/FsSpec","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/farlee2121%2FFsSpec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/farlee2121%2FFsSpec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/farlee2121%2FFsSpec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/farlee2121%2FFsSpec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/farlee2121","download_url":"https://codeload.github.com/farlee2121/FsSpec/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249067779,"owners_count":21207396,"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":["data-generation","type-driven-development","validation"],"created_at":"2024-10-17T15:23:54.967Z","updated_at":"2025-04-15T12:14:57.072Z","avatar_url":"https://github.com/farlee2121.png","language":"F#","readme":"# FsSpec\n\n[![CI Build](https://github.com/farlee2121/FsSpec/actions/workflows/ci.yml/badge.svg)](https://github.com/farlee2121/FsSpec/actions/workflows/ci.yml)\n[![Nuget (with prereleases)](https://img.shields.io/nuget/v/fsspec?label=NuGet%3A%20FsSpec)](https://www.nuget.org/packages/fsspec)\n\nNOTE: Looking for feedback and experiences with the library to smooth it out. Please leave [a comment](https://github.com/farlee2121/FsSpec/issues/2)!\n\n## What is FsSpec and why would you use it?\n\n### Short Motivation\nFsSpec represents value constraints as data to reuse one constraint declaration for validation, data generation, error explanation, and more.\n\nIt also makes for a concise and consistent Type-Driven approach\n```fsharp\nopen FsSpec\ntype InventoryCount = private InventoryCount of int\nmodule InventoryCount = \n    let spec = Spec.all [Spec.min 0; Spec.max 1000]\n    let tryCreate n =\n      Spec.validate spec n \n      |\u003e Result.map InventoryCount\n\n// Generate data\nlet inventoryAmounts = Gen.fromSpec InventoryCount.spec |\u003e Gen.sample 0 10\n```\n\n### Longer Motivation\nType-Driven and/or Domain-Driven systems commonly model data types with constraints. For example, \n- a string that represents an email or phone number (must match format)\n- an inventory amount between 0 and 1000\n- Birthdates (can't be in the future)\n\nWe centralize these constraints by wrapping them in a type, such as\n\n```fsharp\ntype PhoneNumber = private PhoneNumber of string\nmodule PhoneNumber = \n    let tryCreate str =\n      if (Regex(@\"\\d{3}-\\d{4}-\\d{4}\").IsMatch(str))\n      then Some (PhoneNumber str)\n      else None \n```\n\nThis is great. It prevents defensive programming from leaking around the system and clearly encodes expectations on data. It avoids the downsides of [primitive obsession](https://grabbagoft.blogspot.com/2007/12/dealing-with-primitive-obsession.html).\n\nHowever, we're missing out on some power. We're encoding constraints in a way that only gives us pass/fail validation. \nWe have to duplicate constraint information if we want to explain a failed value, generate data, or similar actions.\n\nFsSpec represents these constraints as data so that our programs can understand the constraints on a value. \n\n```fsharp\nlet inventorySpec = Spec.all [Spec.min 0; Spec.max 1000]\n\n// Validation\nSpec.isValid inventorySpec 20\n\n// Explanation: understand what constraints failed (as a data structure)\nSpec.explain inventorySpec -1\n\n// Validation Messages\nSpec.explain inventorySpec -1 |\u003e Formatters.prefix_allresults // returns: \"-1 failed with: and [min 0 (FAIL); max 1000 (OK)]\"\n\n// Data Generation (with FsCheck)\nGen.fromSpec inventorySpec |\u003e Gen.sample 0 10  // returns 10 values between 0 and 1000\n```\n\nThere are also other possibilities FsSpec doesn't have built-in. For example,\n- Comparing specifications (i.e. is one a more constrained version of the other)\n- Serialize and interpret constraints for use in different UI technologies\n- Automatic generator registration with property testing libraries (e.g. FsCheck)\n\n## Spec Composition and Resuse\n\nSpecs are just values which can be stored and composed. \nThis opens up opportunity for readable and reusable data constraints. \n\nFor example, we can break up complex constraints\n\n```fsharp\nlet markdown = //could vary in complexity\nlet sanitizedMarkdown = markdown \u0026\u0026\u0026 //whatever sanitization looks like\nlet recipeIngredientSpec = sanitizedMarkdown \u0026\u0026\u0026 notEmpty \n```\n\nBreaking out sub-constraints improves readability, but also identifies constraints we might reuse, like `markdown` or maybe `FullName`, `FutureDate`, `PastDate`, `NonNegativeInt` etc.\n\n\nSuch constraints can be centralized and reused like any other data (e.g. readonly members of a module). They do not have to be associated with a type, making them fairly light weight.\nThere is also no duplication if such cross-cutting constraints change in the future.\n\n## Basic Value Type using FsSpec\n\nIt's still a good idea to create value types for constrained values. Here's how you might do it with FsSpec\n\n```fsharp\nopen FsSpec\ntype InventoryCount = private InventoryCount of int\nmodule InventoryCount = \n    let spec = Spec.all [Spec.min 0; Spec.max 1000]\n    let tryCreate n =\n      Spec.validate spec n \n      |\u003e Result.map InventoryCount\n```\n\n\n## Supported Constraints\n\n- `Spec.all spec-list`: Logical and. Requires all sub-specs to pass\n- `Spec.any spec-list`: Logical or. Requires at least one sub-spec to pass\n- `Spec.min min`: Minimum value, inclusive. Works for any `IComparable\u003c'a\u003e`\n- `Spec.max max`: Maximum value, inclusive. Works for any `IComparable\u003c'a\u003e`\n- `Spec.regex pattern`: String must match the given regex pattern. Only works for strings. \n- `Spec.predicate label pred`: Any predicate (`'a -\u003e bool`) and a explanation/label\n- `Spec.minLength min`: set a minimum length for a string or any IEnumerable derivative\n- `Spec.maxLength max`: set a maximum length for a string or any IEnumerable derivative\n- `Spec.values values`: an explicit list of allowed values\n- `Spec.notValues values`: an explicit list of disallowed values\n\n## Generation Limitations\n[![Nuget (with prereleases)](https://img.shields.io/nuget/v/fsspec.fscheck?label=NuGet%3A%20FsSpec.FsCheck)](https://www.nuget.org/packages/FsSpec.FsCheck)\n\nData generation can't be done efficiently for all specifications.\nThe library recognizes [special cases](./src/FsSpec.FsCheck/OptimizedCases.fs) and filters a standard generator of the base type for everything else.\n\nSupported cases\n- Common ranges: most numeric ranges, date ranges\n  - Custom scenarios for other IComparable types would be easy to add, if you encounter a type that isn't supported.\n- Regular expressions\n- Logical and/or scenarios\n- String length\n- Collection length: currently support `IEnumerable\u003cT\u003e`, lists, arrays, and readonly lists and collections.\n  - Dictionaries, sets, and other collections are not yet supported but should not be difficult to add if users find they need them.\n- `Spec.values`, an explicit list of allowed values \n  - `Spec.notValues` works by filtering. This will likely fail if the disallowed values are a significant portion of the total possible values\n\nPredicates have limited generation support. For example, \n```fsharp\nlet spec = Spec.predicate \"predicate min/max\" (fun i -\u003e 0 \u003c i \u0026\u0026 i \u003c 5)\n```\nThe above case will probably not generate. It is filtering a list of randomly generated integers, and it is unlikely many of them will be in the narrow range of 0 to 5. FsSpec can't understand the intent of the predicate to create a smarter generator.\n\nImpossible specs (like `all [min 10; max 5]`), also cannot produce generators. The library tries to catch impossible specs and thrown an error instead of returning a bad generator.\n\n## Complex / Composed Types\n\nFsSpec doesn't currently support composed types like tuples, records, unions, and objects.\n\nThe idea is that these types should enforce their expectations through the types they compose. Scott Wlaschin gives a [great example](https://fsharpforfunandprofit.com/posts/designing-with-types-representing-states/) as part of his designing with types series.\n\nA short sample here.\n\nSum types (i.e. unions) represent \"OR\". Any valid value for any of their cases should be a valid union value. The cases themselves should be of types that enforces any necessary assumptions\n```fsharp\ntype Contact = \n  | Phone of PhoneNumber\n  | Email of Email\n```\n\nProduct types (records, tuples, objects) should represent \"AND\". They expect their members to be filled. If a product type doesn't require all of it's members, the members that are not required should be made Options.\n\n```fsharp\ntype Person = {\n  // each field enforces it's own constraints\n  Name: FullName \n  Phone: PhoneNumber option // use option for non-required fields\n  Email: Email option\n}\n```\n\nRules involving multiple members should be refactored to a single member of a type that enforces the expectation. A common example is requiring a primary contact method, but allowing multiple contact methods.\n```fsharp\ntype Contact = \n  | Phone of PhoneNumber\n  | Email of Email\n\ntype Person = {\n  Name: FullName \n  PrimaryContactInfo: Contact\n  OtherContactInfo: Contact list\n}\n```\n\nSee [Designing with Types](https://fsharpforfunandprofit.com/series/designing-with-types/) (free blog series) or the fantastic [Domain Modeling Made Functional](https://fsharpforfunandprofit.com/books/#domain-modeling-made-functional) (book) for more detailed examples.\n\n## What this library is not\n\nThis library *does* look improve programmatic accessibility of data constraints for reuse.\nThe library *can* be used for all kinds of approaches that use constraints on data.\n\nHowever, the library is made with existing Type-driven approaches in mind. \nScott Wlaschin has a great series on [type-driven design](https://fsharpforfunandprofit.com/series/designing-with-types/) if you are not familiar.\n\nThis library is not an extension to F#'s type system. The types representing constrainted values are created as normal using F#'s type system.\nFsSpec works within this approach to make the constraints more accessible, but does not change the overall approach or add additional safety guarantees.\n[F*](https://www.fstar-lang.org/) may be of interest if you need static checks based on constraints.\n\nFsSpec is also not intended for assertions or Design by Contract style constraint enforcement.\nA DbC approach is fairly easy to achieve with FsSpec, but there is no plan to support it natively.\nType-driven is the recommended approach. \n\nType-driven approaches bias systems toward semantic naming of constrained values, centralization of reused constraints, and error handling pushed to the system edge.\nDesign by Contract does not share these benefits.\n\nIf you still desire assertions, here's an example of how it can be done\n\n```fsharp\nmodule Spec = \n  let assert' spec value =\n    let valueExplanation = Spec.explain spec value\n    if Explanation.isOk valueExplanation.Explanation\n    then ()\n    else failwith (valueExplanation |\u003e Formatters.prefix_allresults)\n```\n\nThis could then be used like this\n```fsharp\nlet divide dividend divisor = \n  Spec.assert' NonNegativeInt divisor\n  dividend/divisor\n```\n\nAgain, this assertion-based approach is not recommended. \n\n## Roadmap\n\nThis library is early in development. The goal is to get feedback and test the library in real applications before adding too many features. Please leave a [comment](https://github.com/farlee2121/FsSpec/issues/2) with your feedback.\n\nLines of inquiry include\n\n- Improve customization: Explore how users most often need to extend or modify existing functionality. \n  - add formatting for their custom constraint?\n  - mapping custom errors? / interpreting error scenarios?\n- Identifying base set of constraints that should be built into the library\n- Predicate spec meta: Potentially allow meta separate from predicates so instances of a similar custom constraints can leverage case specific info (e.g. if max were implemented as custom, making the max value accessible to custom formatters, comparisons, generators, etc)\n- Not spec: Negate any specification. \n  - This is easy to add for validation, but makes normalization for inferring generators more complex. It should be doable, but I have to consider negations of specs (i.e. max becomes min, regex becomes ???) and how that would impact other features like explanation\n\n\n## Project Status\nThe most foundational features (validation, generation, explanation) are implemented and tested.\nThe library should be reliable, but the public API is subject to change based on feedback.\n\nThe main goal right now is to gather feedback, validate usefulness, and determine next steps, if any.\n\n## Inspiration\nThis library borrows inspiriation from many sources \n- Type-driven Development\n  - [Designing with Types](https://fsharpforfunandprofit.com/series/designing-with-types/) by Scott Wlaschin\n  - [Mark Seemann](https://blog.ploeh.dk/2015/05/07/functional-design-is-intrinsically-testable/#aee72ce959654d9388b448023f469cbc)\n- [Clojure.spec](https://clojure.org/about/spec)\n- [Specification Pattern](https://www.martinfowler.com/apsupp/spec.pdf) by Eric Evans and Martin Fowler\n- [Domain Driven Design](https://en.wikipedia.org/wiki/Domain-driven_design)\n\n## Original Experiments\n\nI previously looked into adding constraints as a more integrated part of the F# type system. \nThose experiments failed, but are [still available to explore](https://github.com/farlee2121/FsSpec-OriginalExperiment).\n\nIf you want such a type system, you might checkout [F*](https://www.fstar-lang.org/), [Idris](https://www.idris-lang.org/), or [Dafny](https://github.com/dafny-lang/dafny).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffarlee2121%2Ffsspec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffarlee2121%2Ffsspec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffarlee2121%2Ffsspec/lists"}