Coder Social home page Coder Social logo

devtin / duckfficer Goto Github PK

View Code? Open in Web Editor NEW
3.0 3.0 1.0 4.13 MB

Zero-dependencies light-weight library for modeling, validating and sanitizing data 🦆 🐵 👁

Home Page: https://devtin.github.io/duckfficer

License: MIT License

JavaScript 41.41% HTML 56.90% Shell 0.06% CSS 1.59% Handlebars 0.04%
json schema duck-typing data validation coercion parsing

duckfficer's People

Contributors

devtin avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

gitter-badger

duckfficer's Issues

Number validation: min, max, integer, decimals

Would be nice to have some validation sugar for:

  • minimum values
  • maximum values
  • just integers (no decimals)
  • decimal places

autoCast then would:

  • adjust decimal places as necessary (places / integers)
const NewNumber = new Schema({
  type: Number,
  min: 0, // just positive numbers
  max: 100,
  decimalPlaces: 2
})

const err1 = t.throws(() => NewNumber.parse(-0.1))
t.is(err1.message, 'minimum accepted value is 0')

const err2 = t.throws(() => NewNumber.parse(100.1))
t.is(err2.message, 'maximum accepted value is 100')

t.is(NewNumber.parse(1.111), 1.11)
t.is(NewNumber.parse(1.114), 1.11)
t.is(NewNumber.parse(1.115), 1.12)

NewNumber._settings.autoCast = true

t.is(NewNumber.parse('1.114'), 1.11)

Error thrown inconsistency

Describe the bug
Transformers are not being consistent with the errors they are throwing: seems like only the String transformer throws a ValidationError and all other just a regular Error. This makes inconsistence the entire experience.

To Reproduce

const numberType = new Schema({
  type: Number
})

try {
  numberType.parse('no')
}
catch (err) {
  console.log(err instanceof ValidationError) // => false
}

Expected behavior
All transformers should throw a ValidationError.

const numberType = new Schema({
  type: Number
})

try {
  numberType.parse('no')
}
catch (err) {
  console.log(err instanceof ValidationError) // => true
}

Ability of disabling auto-casting

Is your feature request related to a problem? Please describe.
The type auto-cast is a nice feature...

const BooleanSchema = new Schema({
  type: Boolean
})

BooleanSchema.parse(5) // => true

const DateSchema = new Schema({
  type: Date
})

console.log(BooleanSchema.parse('6/11/1983') instanceof Date) // => true

But sometimes it is necessary to prevent value type's auto-casting, making the validation strict.

Describe the solution you'd like
Maybe introducing a configuration value that all transformers must check prior proceeding with auto-casting and just throw an error when the given value is not what expected.

Describe alternatives you've considered

const BooleanSchema = new Schema({
  type: Boolean,
  autoCast: false
})

try {
  BooleanSchema.parse(5)
} catch (err) {
  console.log(err.message) // => Invalid boolean
}

const DateSchema = new Schema({
  type: Date,
  autoCast: false
})

DateSchema.parse(new Date('6/11/1983 23:11 GMT-0400')) // => Sat Jun 11 1983 23:11:00 GMT-0400 (Eastern Daylight Time)

try {
  DateSchema.parse('6/11/1983')
} catch (err) {
  console.log(err.message) // => Invalid date
}

improve docs

Reduce README scroll by using <details><summary></summary></details>

Add more tests

In order to avoid inconsistent releases, add more tests per type.

Each transformer should have a test covering all possible scenarios.

sync version

I know I've been back and forth with this. But feels like there are scenarios where having a sync validation is useful too...

const { SchemaSync } = require('duckfficer')

make virtuals enumerable

feels like if we are setting a schema with a virtual, we are actually demanding that prop on the final payload...

Allow multiple types

Is your feature request related to a problem? Please describe.
Sometimes it is useful to allow a property being any of some given types.

Describe the solution you'd like

const UserSchema = new Schema({
  name: String,
  profilePicture: {
    type: [Function, Promise]
  }
})

Describe alternatives you've considered
Creating custom transformers for each scenario, but that is pretty nasty I think.

unknown error when parsing undefined paths with `null` value

const { Schema } = require('./')

const s = new Schema({
  name: String,
  address: {
    line1: String,
    zip: Number
  }
})

console.log(s.parse({
  name: 'Martin',
  papo: null,
  address: {
    line1: '2451',
    zip: 33129,
  }
}))

throws:

TypeError: Cannot convert undefined or null to object

Review Transformers

Implementation suggested in #6 may be cleaner, hence better to address if the transformers had two methods: one for auto-casting and another for validating.

Transformers = {
  Date: {
    parse(value) {
      // validates given value is of the expected type
    },
    cast (value) {
      // instructions to cast given value (if possible) to expected one
    }
  }
}

treats type as multiple types when a transformer uses loaders

Transformers.Custom = {
  loaders: [{
    type: String,
    allowEmpty: false,
    emptyError: 'Allow empty not valid',
  }]
  parse (v) {
    return v.split('').reverse().join('')
  }
}

const schema = new Schema('Custom')
schema.parse(undefined) // => Could not resolve given value type in property password. Allowed types are  and String

Introduce type `Promise`

Describe the solution you'd like

Transformers.Promise = {
  settings: {
    typeError: `Invalid Promise`
  },
  validate (v) {
    // see: https://stackoverflow.com/a/27746324/1064165
    if (!(typeof v === 'object' && typeof v.then === 'function')) {
      this.throwError(Transformers.Promise.settings.typeError, { value })
    }
  }
}

virtuals

it would be nice to implement virtuals:

const Order = new Schema({
  paymentStatus: {
    type: String,
    enum: ['processing', 'hold', 'approved', 'rejected', 'refunded'],
    default: 'processing'
  },
  shippingStatus: {
    type: String,
    enum: ['preparing', 'shipped', 'delivered', 'returned-issued', 'returned']
    allowNull: true,
    default: null
  }
}, {
  virtuals: {
    get status () {
      if (this.paymentStatus === 'approved') {
        return this.shippingStatus
      }
      return this.paymentStatus
    }
  }
})

const order = Order.parse({})
t.is(order.status, 'processing')
order.paymentStatus = 'approved'
order.status = 'preparing'

see: https://stackoverflow.com/a/34845963/1064165

Object transformer broke (introduced v1.1.2)

Issue

const objectParser = new Schema({
  type: Object
})

try {
  objectParser.parse({ hi: 'hello' })
}
catch (err) {
  console.log(err.message) // => v is not defined
}

Suggestion
Not only adding more tests as suggested in #16 will help catch this kind of issues in the future prior performing a release, but errors of this kind can also be addressed by only performing a linting in the development flow.

Make the flow async

I started building this library using a synchronous philosophy on purposely. At the beginning I felt by staying away from asynchronous tasks I was making the library more simple and elegant, but soon I was proof wrong.

What I feel right now is that we live in an asynchronous world and there is nothing we can do about it, so we better deal with it. Thinking so synchronously is a wing blocker.

How about:

Transformers.BucketFile = {
  loaders: [
    {
      type: String,
      async cast (url) {
        if (isFile(url)) {
          // upload the file to a bucker and retrieve the url
          return (await Bucket.upload(url)).url
        }
        return url
      }
    }
  ],
  validate (url) {
    if (typeof url !== 'string' || !/^https?:\/\//.test(url)) {
      this.throwError(`${ url } is not a valid url
    }
  }
}

const Product = new Schema({
  name: String,
  image: 'BucketFile'
})

const productA = await Product.parse({
  name: 'Some product',
  image: new Buffer({...})
})

console.log(/^https:\/\//.test(productA.image)) // => true

Add `enum` option to `String` Transformer

Describe the solution you'd like

const mySchema = new Schema({
  topping: {
    type: String,
    enum: ['cheese', 'ham', 'tomatoes']
  }
})
const error = t.throws(() => mySchema.parse({ topping: 'potatoes' }))
t.is(error.errors[0].message, 'Unknown enum option potatoes')

Array types can describe the type of items they store

Is your feature request related to a problem? Please describe.
It would be nice that given an array type in the schema, one could also provide the type of value accepted by the array, initializing them in case it can be guessed.

Describe the solution you'd like

const Log = new Schema({
  user: String,
  lastAccess: {
    type: Array,
    items: {
      type: Date,
      autoCast: true
    }
  }
})

const tinLog = Log.parse({
  user: 'tin',
  lastAccess: ['6/11/2019', 'Sat Jan 11 2020 17:06:31 GMT-0500 (Eastern Standard Time)']
})

console.log(Array.isArray(tinLog.lastAccess)) // => true
console.log(tinLog.lastAccess.length) // => 2
console.log(tinLog.lastAccess[0] instanceof Date) // => true
console.log(tinLog.lastAccess[1] instanceof Date) // => true

try {
  Log.parse({
    user: 'tin',
    lastAccess: ['6/11/1983', 'What is love?']
  })
}
catch (err) {
  console.log(err.message) // => Data is not valid
  console.log(err.errors[0].message) // => Invalid date
  console.log(err.errors[0].field.path) // => lastAccess
  console.log(err.errors[0].index) // => 1
}

Extend a schema

It would be nice to be able to extend a schema...

const UserSchema = new Schema({
  name: String,
  email: String,
  password: String
})

const ITUserSchema = UserSchema.extend({
  phoneNumber: Number,
  publicKey: String
})

property to allow a given list of values despite anything

Just like the property allowNull allows any property to be set with a null value, there are times when you need a property to be set with any of a given specific list of values.

Maybe introducing a property called allow where we can allow: [null, '', false] would be nice!

Create allowEmpty option for Strings

The option should default to true in order not to break the current behavior.
It should validate that a string is not an empty string:

// should validate like
function isEmpty (v) {
  return /^[\s]*$/.test(v)
}

Pass default values as an object

Sometimes the default values of a schema are given in an object, making the current way of using it little longer.

const defaultValues = {
  name: 'Martin',
  address: {
    state: 'Florida',
    zip: 33129
  },
  subscribe: true
}
const SomeSchema = new Schema({
  name: {
    type: String,
    default: defaultValues.name
  },
  address: {
    state: {
      type: String,
      default: defaultValues.address.state
    },
    zip: {
      type: Number,
      default: defaultValues.address.zip
    },
    street: String
  },
  phoneNumber: Number,
  subscribe: {
    type: Boolean,
    default: defaultValues.subscribe
  }
})

I think something like this should be better:

const defaultValues = {
  name: 'Martin',
  address: {
    state: 'Florida',
    zip: 33129
  },
  subscribe: true
}
const SomeSchema = new Schema({
  name: String,
  address: {
    state: String,
    zip: Number,
    street: String
  },
  phoneNumber: Number,
  subscribe: Boolean
}, { defaultValues })

Improve readme

  • Add a ‘how it works’ section that is willing to explain the logic as a wholesome
  • Improve feature titles by making them easier to read: some of them are repetitive and long
  • Maybe add a description per feature prior the code, explaining its value
  • Keep a core benchmark for core features
  • Create a transformers section where to dive deep into each trasnformer’s feature / settings / examples.
  • Move those features who’s value is added by the transformer, to the transformer itself (keep clean and separate core features from transformers features)

Provide a state during parsing for validation logic

While performing validation it would be useful to provide a payload to alternatively use in custom validation phases:

const UserSchema = new Schema({
  id: BigInt,
  name: String,
  email: String,
  status: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user',
    validate(v, payload) {
      if (v === 'admin' && (!payload || !payload.user || payload.user.status !== 'admin')) {
        this.throwError(`Only admin users can set status to admin`)
      }
    }
  }
})

const payload = {
  user: {
    id: 11,
    name: 'Martin',
    email: '[email protected]',
    status: 'admin'
  }
}

UserSchema.parse({
  name: 'Olivia',
  email: '[email protected]',
  status: 'admin'
}, payload)

Date type does not throw an error given an invalid date

Describe the bug
Given an invalid date, the schema validator does not throw an error.

To Reproduce

const dateValidator = new Schema({
  type: Date
})

console.log(dateValidator.parse('Some invalid date')) // => Invalid Date

Expected behavior

const dateValidator = new Schema({
  type: Date
})

try {
  dateValidator.parse('Some invalid date')
} catch (err) {
  console.log(err.message) // => Invalid date
}

Improve docs

After using the library for a while, I think the documentation can be improved by organizing it in different categories:

Schema

  • Creating a schema
  • Validating / sanitizing arbitrary objects
  • Error handling
  • Required / optional values
  • Default values
  • Null values
  • Nesting schemas
  • Multiple types
  • Loaders

Validation

  • Built-in validation (provided by types / transformers)
  • Custom property validation hook (provided at schema-setting level)
  • Custom value validation hook (provided at schema level)

Casting (sanitation)

  • Built-in cast (provided by types / transformers)
  • Custom property cast hook (provided at schema-setting level)
  • Custom value cast hook (provided at schema level)

Types (transformers)

  • Array
  • BigInt
  • Boolean
  • Date
  • Function
  • Number
  • Object
  • Promise
  • Set
  • String
  • Creating a custom type

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.