Nuclear Powered Marshmallows

    fictional
    TypeScript icon, indicating that this package has built-in type declarations

    0.7.7 • Public • Published

    fictional

    Generate fake data deterministically from a given input

    import { word } from 'fictional'
    
    word('id-1')
    // =>
    'Minanȯ'
    
    word('id-2')
    // =>
    'Rayuấ'
    
    word('id-1')
    // =>
    'Minanȯ'
    import { words, shape } from 'fictional'
    
    const user = shape({
      name: words.options({ capitalize: 'all' })
    })
    
    user('id-1')
    // =>
    {
      name: 'Nįna Kaîmehyko'
    }
    
    user('id-2')
    // =>
    {
      name: 'Ḣakenoyu Socẖi Shỉ'
    }
    
    user('id-1')
    // =>
    {
      name: 'Nįna Kaîmehyko'
    }

    # Why? How is this different to faker?

    Libraries like faker or chance allow you to seed a psuedo-random number generator (PRNG), such that the same sequence of values will be generated every time. If that is all you need, those libraries are for you.

    Sometimes though, instead of needing to obtain the same sequence of generated values every time, from some set of identifiers, you need to obtain the same mapping to generated values every time. This is where fictional comes in.

    For example, when generating api response data in tests, you need to rely on each field always mapping to the same generated value every test run. To illustrate, you could use fictional to generate some user entity in a test:

    import { word, shape, int } from 'fictional'
    
    const name = shape({
      first: word,
      last: word
    })
    
    const user = shape({
      id: int,
      name
    })
    
    user('id-1')
    // =>
    {
      id: 677947713,
      name: {
        first: 'Kaiƴo',
        last: 'Yoḱitame'
      }
    }

    To some extent, there are ways of achieving similar results with libraries like faker, but we haven't found ways that do not have practical limitations:

    • It is possible to simply seed the PRNG for every identifier, and then use it to generate only a single value. This seems to be a misuse of these libraries though: there is an up-front cost to seeding these PRNGs that can be expensive if done for each and every value to be generated. Here are benchmarks that point to this up-front cost.
    • You can generate a sequence of N values, hash identifiers to some integer smaller than N, then simply use that as an index to lookup a value in the sequence. This can even be done lazily. Still, you're now limiting the uniqueness of the values to N. The larger N is, the larger the cost of keeping these sequences in memory, or the more computationally expensive it is if you do not hold onto the sequences in memory. The smaller N is, the less unique your generated values are.

    # API Overview

    # Makers

    Fictional provides functions that take in some identifying value as input, and generate a corresponding output value. These functions are called makers.

    import { word } from 'fictional'
    
    // `word` is a maker
    word('id-1')
    // =>
    'Minanȯ'

    The given input can be any JSON-serializable value. For any two calls to the same maker function, provided the input given in each call serializes down to the same value, the same output will be returned. Makers work statelessly, so for the same input, the same value will be returned regardless of the enviornment, process, call ordering, or any other external factors.

    Note that unlike JSON.stringify(), object property ordering is not considered.

    import { word } from 'fictional'
    
    word({
      a: 21,
      b: 23
    })
    // =>
    'Ṽitame'
    
    word({
      b: 23,
      a: 21
    })
    // =>
    'Ṽitame'

    # Composition

    const streetAddress = join(' ', [
      int.options({
        min: 1,
        max: 200
      }),
      word,
      oneOf(['Drive', 'Street', 'Avenue'])
    ])
    
    streetAddress('id-1')
    // =>
    '46 Ṁuso Street'
    
    streetAddress('id-2')
    // =>
    '80 Ceahÿ Street'

    Some makers take in identifying value as the only required argument and return. These kinds of makers are described in the docs as primary makers. word() is an example of such a maker.

    However, sometimes the data you need generated requires a combination of different makers. Fictional provides functions for doing this: they take in an identifying value and makers as arguments, and compose these makers in some way to produce a corresponding output. These kinds of makers are described in the docs as composition makers. join() (shown above) is an example of a such a maker.

    In the example above, a maker returning fictitious street addresses is formed by using join() to compose int, word(), and other composing maker, oneOf.

    Under the hood, composition makers re-hash the identifying value each time a maker is given as input is used. This ensures that a unique value is generated for each maker provided, while still keeping the result deterministic. In the example below, the tuple() maker ensures that each word in the returned array has a different value.

    tuple('id-1', [word, word])
    // =>
    [
      'Șhihyceavi',
      'Ṁuso'
    ]
    
    // this is roughly the same as doing
    [word(hash('id-1')), word(hash(hash('id-1')))]

    # Options

    Many makers accept an options object as an argument for configuring how the generated output looks:

    int('id-1')
    // =>
    3781622359
    
    int('id-1', {
      min: 1,
      max: 99
    })
    // =>
    65

    As a convenience, it is also possible to extend these makers to use specific options by using the .options() api:

    const newInt = int.options({
      min: 1,
      max: 99
    })
    
    newInt('id-1')
    // =>
    65
    
    newInt('id-2')
    // =>
    61

    .options() returns a new function that will call the original maker function with the given arguments. It is still possible to provide options when calling the returned function. In this case, these options will override any options given to .options():

    const newInt = int.options({
      min: 1,
      max: 99
    })
    
    newInt('id-1', { max: 3 })
    // =>
    2

    .options() can also be called on the returned function, to further extend the maker:

    const newInt = int
      .options({ min: 1 })
      .options({ max: 99 })
    
    newInt('id-1')
    // =>
    65
    
    newInt('id-2')
    // =>
    61

    # Currying

    Composition makers take in more than one required argument. If the identifying input value is not given as an argument (one less than the required arguments is provided), then a new function will be returned. This function will take an identifying input value as its only argument, and call the original composition maker with both this argument and the other required arguments initially given. This limited form of currying can be convienent for composing makers:

    const companyName = join(' ', [
      word,
      oneOf(['Incorporated', 'Systems'])
    ])
    
    companyName('id-1')
    // =>
    'Șhihyceavi Systems'
    
    companyName('id-2')
    // =>
    'Raeyuraḱe Systems'

    # API Reference

    # Primaries

    # int(input[, options])

    Takes in an identifying input value and returns an integer.

    int('id-23')
    // =>
    292896007
    options
    • min=1 and max=Number.MAX_SAFE_INTEGER: the minimum and maximum possible values for returned numbers
    int('id-2', {
      min: 2,
      max: 99
    })
    // =>
    8

    # bool(id)

    Takes in an identifying input value and returns a boolean.

    bool('id-23')
    // =>
    true

    # float(id[, options])

    Takes in an identifying input value and returns a number value with both a whole and decimal segment.

    float('id-23')
    // =>
    3710813343.2980433
    options
    • min=1 and max=Number.MAX_SAFE_INTEGER: the minimum and maximum possible values for returned numbers
    float('id-2', {
      min: 2,
      max: 99
    })
    // =>
    84.103263

    # dateString(id[, options])

    Takes in an identifying input value and returns a string representing a date in ISO 8601 format.

    dateString('id-23')
    // =>
    '1987-08-20T07:13:44.000Z'
    options
    • minYear=1980 and maxYear=2019: the minimum and maximum possible year values for returned dates
    dateString('id-2', {
      minYear: 1980,
      maxYear: 2089
    })
    // =>
    '2062-01-21T12:25:17.000Z'

    # char(input)

    Takes in an identifying input value and returns a string with a single character.

    char('id-23')
    // =>
    '9'

    The generated character will be an alphanumeric: lower and upper case ASCII letters and digits 0 to 9. Alternative character ranges are listed below. To choose your own range of characters, see char.inRanges().

    Predefined character ranges
    char.ascii('id-2')
    // =>
    'Y'
    
    char.digit('id-3')
    // =>
    '3'

    Fictional ships with makers for a predefined set of character ranges. Similar to char(), these makers take in only an identifying input value as an argument and return a string with a single character. The following ranges are available:

    • char.ascii: Any ASCII character

    • char.digit: Characters for numbers 0 to 9

    • char.alphanumeric (alias: char): lower and upper case ASCII letters and digits 0 to 9

    • char.letter (alias: char.asciiLetter): Lower and upper case ASCII letters

    • char.lower (alias: asciiLower): Lower case ASCII letters

    • char.upper (alias char.asciiUpper): Upper case ASCII letters

    • char.unicode: Any character from the ASCII and Latin-1 Supplement unicode blocks

    • char.unicodeLetter: Lower and upper case letters from the ASCII and Latin-1 Supplement unicode blocks

    • char.unicodeLower: Lower case letters from the ASCII and Latin-1 Supplement unicode blocks

    • char.unicodeUpper: Upper case letters from the ASCII and Latin-1 Supplement unicode blocks

    • char.latin1: Any character from the Latin-1 Supplement unicode block

    • char.latin1Letter: Lower and upper case Latin-1 Supplement letters

    • char.latin1Lower: Lower case Latin-1 Supplement letters

    • char.latin1Upper: Upper case Latin-1 Supplement letters

    # char.inRanges(ranges)

    Takes in an array of [min, max] pairs, where min and max are integers specifying the minimum and maximum possible Unicode code point values for a desired range of characters, and returns a maker function that will return characters in those given ranges.

    const symbols = char.inRanges([
      // misc symbols
      [0x2600, 0x26ff],
    
      // emoticons
      [0x1f600, 0x1f64f]
    ])
    
    symbols('id-1')
    // =>
    '⚗'

    char.inRanges is designed to allow characters in the ranges given to all have a similar likelihood of being returned.

    To allow for composition, each item in the array of ranges can also be a pre-defined character range, or another character range defined using char.inRanges():

    const misc = char.inRanges([[0x2600, 0x26ff]])
    const emoticons = char.inRanges([[0x1f600, 0x1f64f]])
    const letterOrSymbol = char.inRanges([misc, emoticons, char.letter])
    
    letterOrSymbol('id-2')
    // =>
    '⚐'

    # word(id[, options])

    Takes in an identifying input value and returns a string value resembling a fictitious word.

    word('id-23')
    // =>
    'Mikẻmu'
    options
    • capitalize=true: whether or not the word should start with an upper case letter
    • unicode=true: whether or not the string should contain non-ascii unicode characters. If true is given, each returned word will always contain a single unicode character. If false is given, each returned word will never contain non-ascii characters. If a value between 0 and 1 is given, that value will represent the probability of a returned value containing a single unicode character.
    • minSyllables=2 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain
    word('id-2', {
      minSyllables: 1,
      maxSyllables: 6,
      unicode: 0.382
    })
    // =>
    'Rayuashira'

    # words(id[, options])

    Takes in an identifying input value and returns a string value resembling fictitious words.

    words('id-23')
    // =>
    'Vạmu kekaicḧi yǫ'
    options
    • min=2 and max=3: the minimum and maximum possible number of words that returned strings will contain.
    • capitalize='first': whether or not the words should start with upper case letters. If true or 'all' is given, each string returned will start with an upper case letter in each word. If 'first' is given, for each string returned, only the first word will start with an upper case letter. If false is given, each string returned will always contain only lower case letters.
    • unicode=true: whether or not the string should contain non-ascii unicode characters. If true is given, each returned word will always contain a single unicode character. If false is given, each returned word will never contain non-ascii characters. If a value between 0 and 1 is given, that value will represent the probability of a returned value containing a single unicode character.
    • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain
    words('id-2', {
      min: 5,
      max: 8,
      unicode: 0.618,
      capitalize: 'all'
    })
    // =>
    'Shinomehy Hẩceaso Kenǒ Řa Kḯn'

    # sentence(id[, options])

    Takes in an identifying input value and returns a string value resembling a sentence of fictitious words.

    sentence('id-23')
    // =>
    'Ma rae soraeta viʈamoki ni mashikeyo vami ko.'
    options
    • minClauses=1 and maxClauses=2: the minimum and maximum possible number of clauses that a returned sentence will contain.
    • minWords=5 and maxWords=8: the minimum and maximum possible number of words that each clause will contain.
    • unicode=0.382: whether or not the string should contain non-ascii unicode characters. If true is given, each returned word will always contain a single unicode character. If false is given, each returned word will never contain non-ascii characters. If a value between 0 and 1 is given, that value will represent the probability of a returned value containing a single unicode character.
    • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain
    sentence('id-2', {
      minClauses: 2,
      maxClauses: 3,
      minWords: 2,
      maxWords: 3,
      unicode: 0.9
    })
    // =>
    'Meami nomakeꝁi viraṅi, noyȗma nọchiso tasomæ.'

    # paragraph(id[, options])

    Takes in an identifying input value and returns a string value resembling a paragraph of fictitious words.

    paragraph('id-23')
    // =>
    'Mu kovahyki nokano kehykicea na mŭraema keshikera, ceǟmo shimokena ræceaso ko murakimo maẖyni mō mekaiyu. Kayohyma vita meyotami shisohẏki yu móyuvimu mishihy yuhy. Rachika yusoramu kairae me hynikaimo shi, kikoceamo yu muashiso raeshi yu kaceaki ha. Ḿi sohaḩy metakeso ṟae ṿaso ceaniha vivamo makocea. Ka sovẩ shikaishịke yo meki, kincea mora machia mo shiyu. Mikë kåira ṽi korachį kinanȍ nakin soraevi yukira, mohy raeyṵ hamukin kavi kinmamu raeniyuni. Nitako s̈hihayo chikovirae mumekaimo kima cę̃asokano.'
    options
    • minSentences=3 and minSentences=7: the minimum and maximum possible number of sentences that a returned paragraph will contain.
    • minClauses=1 and maxClauses=2: the minimum and maximum possible number of clauses that each sentence will contain.
    • minWords=5 and maxWords=8: the minimum and maximum possible number of words that each clause will contain.
    • unicode=0.382: whether or not the string should contain non-ascii unicode characters. If true is given, each returned word will always contain a single unicode character. If false is given, each returned word will never contain non-ascii characters. If a value between 0 and 1 is given, that value will represent the probability of a returned value containing a single unicode character.
    • minSyllables=1 and maxSyllables=4: the minimum and maximum possible number of syllables that returned words will contain
    paragraph('id-2', {
      minSentences: 2,
      minSentences: 3,
      unicode: 0.9
    })
    // =>
    'Ǹi ceami vakinkaȳo kimaṽi răshi nṓka mʉ ceamikąi. Vìkoaso mekashɨso kaćhi mehyǩin mashirąe soyuraevi kaiyuŗa. Nokê̄ raě maso kekanȱ sochi raƙeko chiñoshi mã, ᶄo taꝁinkahy yǚkakoka misȭceavi kȇ. Kiyuko kế kȉnmusova ḱinha shì. Koñami yokaiḣyra ňoshi soayuso ninaviḥy raķinmamu kǟi, ḱorahy vikaiyủ sħi m̃ora ꞧamimeyu nò ṛae taᶄinrae. Niyoraeýo ḱi yumǿ kovami ṥhiraeshike. Chikã ᶄi kaį̃ niňovirae soḵaiva ḿeyu cėako.'

    # Composition

    # join(input, joiner, values)

    Takes in an identifying input value and an array of makers as values, calls each with a unique identifying input, and joins the results with the given joiner.

    join('id-23', ' ', [word, oneOf(['Street', 'Drive'])])
    // =>
    'Kinshiẏora Street'

    If an item in the value array is not a function, that value will be used as-is:

    join('id-2', ' ', [word, 'Drive'])
    // =>
    'Raeyuraḱe Drive'

    joiner can also be a function, in which case it will be called with the results of resolving each item in values as input:

    join('id-3', ([a, b, c]) => `${a}-${b} ${c}`, [word, word, word])
    // =>
    'Hakehysḩi-Mḯanokin Chisọkayu'

    If any of the items in values resolves to a nested array, that array will be flattened (regardless of nesting depth):

    join('id-2', '', [
      char.letter,
      times(3, char.alphanumeric)
    ])
    // =>
    'vqD6'

    # oneOf(input, values)

    Takes in an identifying input value and an array of values, and returns an item in values that corresponds to that input:

    oneOf('id-23', ['red', 'green', 'blue'])
    // =>
    'blue'

    If an item in values is a maker, that maker will be called and the result will be returned:

    oneOf('id-2', [int, word, char])
    // =>
    'i'

    # someOf(input, range, values)

    Takes in an identifying input value and an array of values, repeatedly picks items from that array a number of times within the given range. Each item will be picked no more than once.

    someOf('id-23', [1, 2], ['red', 'green', 'blue'])
    // =>
    [
      'green'
    ]

    As shown above, range can be a tuple array of the minimum and maximum possible number of items that can be picked.

    It can also be given as a number, in which case exactly that number of items will be picked:

    someOf('id-2', 2, ['red', 'green', 'blue'])
    // =>
    [
      'blue',
      'red'
    ]

    If an item in values is a maker, that maker will be called and the result will be returned:

    someOf('id-3', [1, 2], [int, word, char])
    // =>
    [
      2310357836,
      'w'
    ]

    # times(input, range, maker)

    Takes in an identifying input value and a maker, calls that maker repeatedly (each time with a unique input) for a number of times within the given range, and returns the results as an array:

    times('id-23', [4, 5], word)
    // =>
    [
      'Haṋihy',
      'Nṏhano',
      'Kaîmokai',
      'Maḉeamita',
      'Vametã'
    ]

    As shown above, range can be a tuple array of the minimum and maximum possible number of times the maker should be called. It can also be given as a number, in which case the given maker will be called exactly that number of times:

    times('id-2', 2, word)
    // =>
    [
      'Ḿamoviso',
      'Noṅi'
    ]

    # tuple(input, values)

    Takes in an identifying input value and an array of makers as values, calls each with a unique identifying input, and returns the array of results.

    tuple('id-23', [char, char])
    // =>
    [
      'R',
      'w'
    ]

    If an item in the value array is not a function, that value will be used as-is:

    tuple('id-2', [char, '!'])
    // =>
    [
      'N',
      '!'
    ]

    # shape(input, properties)

    Takes in an identifying input value and an object of makers as properties, calls each property's value with a unique identifying input, and returns results as an object.

    shape('id-23', {
      firstName: word,
      lastName: word
    })
    // =>
    {
      firstName: 'Nimȫ',
      lastName: 'Muhẩmimo'
    }

    If an item in the properties object is not a function, that value will be used as-is:

    shape('id-23', {
      name: join(' ', [word, word]),
      active: true
    })
    // =>
    {
      name: 'Kim̃uhy Ṽivakinchi',
      active: true
    }

    # oneOfWeighted(id, values)

    Takes in an identifying input value and a value array of consisting of [probability, value] pairs, and returns one of one of the values in that array. The likelihood of a particular value being returned will correspond to the probability given for it, where probability is a number between 0 and 1.

    oneOfWeighted('id-23', [
      [0.9, 'red'],
      [0.05, 'green'],
      [0.05, 'blue']
    ])
    // =>
    'red'

    If an item in values is a maker, that maker will be called and the result will be returned:

    oneOfWeighted('id-2', [
      [0.9, word],
      [0.05, char],
      [0.05, int]
    ])
    // =>
    'Ÿutakinki'

    For each [probability, value] pair in the array of values, if the given probability is not a number, that probability will be considered unassigned. All items with unassigned probabilities will receive an equal share of the remaining probability after accounting for all items with assigned probabilities (all items for which a number value was given for their probability). In the example below, 'green' and 'blue' will both have a probability of 0.4 of being returned ((1 - 0.2) / 2).

    oneOfWeighted('id-23', [
      [0.2, 'red'],
      [null, 'green'],
      [null, 'blue']
    ])
    // =>
    'green'

    # Install & Use

    You can use fictional as the npm package fictional:

    npm i -D fictional  # chances are you want it as a devDependency
    # or
    yarn add -D fictional
    

    Fictional can be used in both es-module-aware and commonjs bundlers/environments.

    // es module
    import { word } from 'fictional'
    
    // or alternatively
    import word from 'fictional/word'
    
    // commonjs
    const { word } = require('fictional')
    
    // or alternatively
    const word = require('fictional/word')

    It can also be used a <script>:

    <script crossorigin src="https://unpkg.com/fictional/dist/umd/fictional.js"></script>
    
    <script>
      fictional.word('some-identifier')
    </script>

    Install

    npm i fictional

    DownloadsWeekly Downloads

    3,438

    Version

    0.7.7

    License

    MIT

    Unpacked Size

    406 kB

    Total Files

    45

    Last publish

    Collaborators

    • justinvdm