Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Style Guide

Josh Field edited this page Feb 3, 2022 · 24 revisions

Writing Composable, Extensible and Testable Functions

In all cases. it is best to write lots of small functions, rather than large functions or classes. This allows us to easily create test cases for each function. These functions can then be overridden and extended by projects to enable easy modifications to the base functionality. The functional paradigm also enables a data-oriented approach.

Functions should not rely on external state where possible, such as mutating variables outside of the function's scope (see Pure vs Impure Functions). The exception is when a function's purpose is to mutate state, such as a FLUX pattern receiver, a database mutation, or ECS interaction.

Provide function-specific extensibility by introducing module exporters

// CustomMathUtil.ts
// modules files must be named identitcally to their module, while lone function files must be named identically to the function they export

const addOne = (i: number) => {
  return i++
}

// functions must be camelCase
const subtractOne = (i: number) => {
  return i--
}

// modules must be CapitalCase
export const CustomMathUtil = {
  addOne,
  subOne
}

Testing

describe('CustomMathUtil', () => {

  describe('addOne', () => {
    
    // first test case
    it('should add one', () => {

      const result = addOne(1)
      assert.equal(result, 2)

    })

    // second test case
    it('should return NaN to non-number', () => {

      const result = addOne(undefined!)
      assert.equal(result, NaN)

    })

  })

})

An example of these stylings can be found as an initial draft at https://github.com/XRFoundation/XREngine/compare/network-action-receptors-follow-style-guide

Example

user.class.ts

export class User extends Service {
  app: Application
  docs: any

  constructor(options: Partial<SequelizeServiceOptions>, app: Application) {
    super(options)
    this.app = app
  }

  async find(params: Params): Promise<any> {
    if (!params.query) params.query = {}
    const { action, $skip, $limit, search, ...query } = params.query!

    const skip = $skip ? $skip : 0
    const limit = $limit ? $limit : 10

    delete query.search

    if (action === 'friends') {
      delete params.query.action
      const loggedInUser = extractLoggedInUserFromParams(params)
      const userResult = await (this.app.service('user') as any).Model.findAndCountAll({
        offset: skip,
        limit: limit,
        order: [['name', 'ASC']],
        include: [
          {
            model: (this.app.service('user-relationship') as any).Model,
            where: {
              relatedUserId: loggedInUser.id,
              userRelationshipType: 'friend'
            }
          }
        ]
      })

      params.query.id = {
        $in: userResult.rows.map((user) => user.id)
      }
      return super.find(params)
    } else if (action === 'layer-users') {
      delete params.query.action
      const loggedInUser = extractLoggedInUserFromParams(params)
      params.query.instanceId = params.query.instanceId || loggedInUser.instanceId || 'intentionalBadId'
      return super.find(params)
    }  
    // ...
  }
}

becomes...

export const userServiceFindActionFriends = async (user: User, params: Params) => {
  const skip = $skip ? $skip : 0
  const limit = $limit ? $limit : 10
  delete params.query.action
  const loggedInUser = extractLoggedInUserFromParams(params)
  const userResult = await (user.app.service('user') as any).Model.findAndCountAll({
    offset: skip,
    limit: limit,
    order: [['name', 'ASC']],
    include: [
      {
        model: (user.app.service('user-relationship') as any).Model,
        where: {
          relatedUserId: loggedInUser.id,
          userRelationshipType: 'friend'
        }
      }
    ]
  })

  params.query.id = {
    $in: userResult.rows.map((user) => user.id)
  }
  return Service.prototype.find.call(user, params)
}

export const userServiceFindActionLayerUsers = async (user: User, params: Params) => {
  delete params.query.action
  const loggedInUser = extractLoggedInUserFromParams(params)
  params.query.instanceId = params.query.instanceId || loggedInUser.instanceId || 'intentionalBadId'
  return Service.prototype.find.call(user, params)
}

export class User extends Service {
  app: Application
  docs: any

  constructor(options: Partial<SequelizeServiceOptions>, app: Application) {
    super(options)
    this.app = app
  }

  async find(params: Params): Promise<any> {
    if (!params.query) params.query = {}
    const { action, $skip, $limit, search, ...query } = params.query!
    delete query.search
  
    if (action === 'friends') {
      return userServiceFindActionFriends(this, params)
    } else if (action === 'layer-users') {
      return userServiceFindActionLayerUsers(this, params)
    }  
    // ...
  }
}
Clone this wiki locally