Ballot was Holium's first attempt at a production-grade app written in the Hoon language. It was a design decision early on in the development process to make heavy of use of json. There were a few different reasons for this decision:
the json structures in hoon are extremely flexible. json gave us a generalized key/value pair store with support for multiple value types (e.g. string %s, object %o, number %n, array %a, etc.). This was very powerful but more importantly did not lock us into a rigid structure like concrete explicit classes. Since we were not yet fully familiar with the language, this flexibility was important; limiting the need for big code refactors when pivots were needed.
coming from product and web development projects, json is familiar and works well both in Hoon and in web applications
eliminated the need for %mark files; which added unnecessary complexity to the code base for a new development team such as ours
gave us the ability to validate our incoming POST requests and provide more robust error handling; such as those needed when writing REST APIs that depend on json as inputs. We found when using mark files, error handling was difficult; often times having code crash deep in the bowels of Urbit when mark/json was invalid. This made it difficult to find the root of the problem, and more importantly made it difficult to gracefully handle errors by providing callers with error information with enough context to fix errors themselves.
Even though Ballot does make heavy use of json, there are a few concrete data types and/or structures that have been defined to carry out function.
plugin.hoon and ballot.hoon can both be found in the <desk>/sur folder of your ship. Descriptions of the types found within these files are described below.
plugin.hoon
call-context - not currently used. may be used in future versions.
action-result - all custom action handlers are required to return action-result objects. the calling driver attempts to cast custom action return values to values of this type. If this type is not returned by the handler, the agent will crash.
+$ action-result
:: %.n if the custom action handler failed; otherwise %.y
$: success=?
:: contains error data when success is %.n; otherwise contains response data data
data=json
:: currently unused. may be used in future versions.
store=json
:: any effects (poke, gift, etc...) that should be delivered when the
:: handler completes
effects=(list card:agent:gall)
==
ballot.hoon
signature - used when signing votes and or delegating votes. signatures allow us to ensure the integrity of the voting process.
This type contains a reference to all of the various stores supported by the Ballot app. For the sake of coding robust custom actions, you may need to access some of these sub-stores, specifically: booths, proposals, participants listed below:
Booths
key/value pair of booth key -> booth detail data (hoon json)
Booths Data - Spec
The booth value (json) is as follows:
{
:: slug - relative path of the group
[p='slug' q=[%s p='<relative path here>']]
:: owner - the booth owner is the booth's host ship; i.e.
:: the ship or group the booth was created under
[p='owner' q=[%s p='<booth-owner here>']]
:: type - can be 'group' or 'ship'
[p='type' q=[%s p='<type here>']]
:: created - number value (epoch timestamp). seconds since midnight Jan 01, 1970
[p='created' q=[%n p=<timestamp here>]]
:: defaults - default booth settings
:: duration - the default period of time a vote will be active once opened
:: and before being closed
:: support - the minimun percentage of votes required before a voting result
:: is considered valid
[p='defaults' q=[%o p={[p='duration' q=[%n p=~.7]] [p='support' q=[%n p=~.50]]}]]
:: adminPermissions - admin permissions within the booth
[ p='adminPermissions'
q
[ %a
p
~[
:: read-proposal - admins can read/view proposals
[%s p='read-proposal']
:: vote-proposal - admins can vote on proposals
[%s p='vote-proposal']
:: create-proposal - admins can create proposals
[%s p='create-proposal']
:: edit-proposal - admins can edit proposals
[%s p='edit-proposal']
:: delete-proposal - admins can delete proposals
[%s p='delete-proposal']
:: invite-member - admins can invite new members to a booth
[%s p='invite-member']
:: remove-member - admins can kick members from a booth
[%s p='remove-member']
:: change-settings - admins can modify booth settings
[%s p='change-settings']
]
]
]
:: memberPermissions - member permissions within the booth. these
:: can be modified by users with the correct privileges, but below
:: are the default permissions
[p='memberPermissions'
q=[%a p=~[
:: read-proposal - member can read/view proposals
[%s p='read-proposal']
:: vote-proposal - member can vote on proposals
[%s p='vote-proposal']
:: create-proposal - member can create proposals
[%s p='create-proposal']
]]]
:: permissions - the permission classes/categories supported by the booth
:: indicates only admin/member permissions supported
[p='permissions' q=[%a p=~[[%s p='member'] [%s p='admin']]]]
:: key - the booth unique identifier (key)
[p='key' q=[%s p='<key here>']]
:: image - currently unused
[p='image' q=~]
:: name - the user-friendly name of the booth as displayed in the UI
[p='name' q=[%s p='<user-friendly name here>']]
:: policy - currently all booths are 'invite-only'. you can only participant
:: in a booth/proposal vote if have been invited by the booth owner or admin`
[p='policy' q=[%s p='<policy here>']]
:: status - booth status
:: can be one of the following: ['active', 'invited', 'pending']
:: active - the booth is currently active; meaning you can participant in voting
:: invited - the ship has received an invite to the booth but has not yet
:: accepted the invite
:: pending - the ship has accepted an invite but not yet received acknowledgement
:: that the acceptance succeeded
[p='status' q=[%s p='<state here>']]
}
map of booth (key) -> proposal (key) -> proposal detail
With a booth-key, you can obtain a dictionary (map @t json) of proposals where each key in the dictionary is a proposal-key.
Proposal Hoon - Spec
[ %o
p
{ :: proposal owner - the ship that created the proposal
[p='owner' q=[%s p='<owner here>']]
:: content - proposal content (markup) as pure text as entered
:: into the proposal editor UI
[p='content' q=[%s p='<content here>']]
:: redacted - has the proposal been redacted %.y or %.n
[p='redacted' q=[%b p=<yes or no>]]
:: end - the timestamp of the end date/time of the proposal vote
[p='end' q=[%n p=<timestamp here>]]
:: created - the date this proposal was created
[p='created' q=[%n p=<timestamp here>]]
:: choices - a list of choices a user can make in response
:: to a given proposal.
:: defaults include: 'Approve' and 'Reject'
[ p='choices'
q
[ %a
p
~[
[ %o
p
{ :: action - the custom action to invoke
[p='action' q=[%s p='<action here>']]
:: data - the action data to pass to the custom action. this will
:: depend on the custom action use-case. see the sample below for
:: an idea as to how this is used for the invite-member custom action
[p='data' q=[%o p={[p='<key>' q=[%s p='<value>']]}]]
:: label - the label of the choice
[p='label' q=[%s p='<label here>']]
}
]
]
]
]
:: voting strategy - ballot currently supports 'single-choice'
[p='strategy' q=[%s p='<strategy here>']]
:: voting support - percentage of votes needed (based on # of participants)
:: to indicate a valid vote
[p='support' q=[%n p=<support here>]]
:: proposal key - unique proposal identifier
[p='key' q=[%s p='<key here>']]
:: proposal title - user friendly title displayed in UI
[p='title' q=[%s p='<title here>']]
:: start date/time - proposal start timestamp (number)
[p='start' q=[%n p=<timestamp here>]]
:: proposal status
:: one of: ['scheduled', 'poll-opened', 'poll-closed']
[p='status' q=[%s p='<status here>']]
:: when a proposal vote closes, the results of the vote are
:: calculated, and these results are stuff into this proposal
:: store under the 'tally' element
[ p='tally'
q
[ %o
p
{
:: the winning choice (e.g. 'Approve' or 'Reject')
[p='topChoice' q=[%s p='<choice label here>']]
:: # of participants that voted on the proposal
[p='participantCount' q=[%n p=<count here>]]
:: array of tally objects that record the stats of each proposal choice
[ p='tallies'
q
[ %a
p
~[
[ %o
p
{ :: percentage of votes this choice received
[p='percentage' q=[%n p=<percentage here>]]
:: custom action associated with the choice
[p='action' q=[%s p='<custom action here>']]
:: # of participants that voted on this choice
[p='count' q=[%n p=<count>]]
:: choice label (e.g. 'Approve' or 'Reject')
[p='label' q=[%s p='<label>']]
}
]
]
]
]
:: total # of votes cast for this proposal
[p='voteCount' q=[%n p=<vote count here>]]
:: tally status
:: one of: ['counted', 'failed']
[p='status' q=[%s p='<status here>']]
}
]
]
}
]