{"id":18319785,"url":"https://github.com/redding/sanford-protocol","last_synced_at":"2025-10-06T17:54:11.213Z","repository":{"id":5496292,"uuid":"6694976","full_name":"redding/sanford-protocol","owner":"redding","description":"Ruby implementation of the Sanford TCP communication protocol.","archived":false,"fork":false,"pushed_at":"2018-04-04T22:45:39.000Z","size":90,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-10-01T09:54:20.112Z","etag":null,"topics":["ruby","sanford","tcp","tcp-protocol"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"grafana/grafana","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/redding.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-11-14T21:10:53.000Z","updated_at":"2018-10-20T00:44:09.000Z","dependencies_parsed_at":"2022-07-07T19:08:39.533Z","dependency_job_id":null,"html_url":"https://github.com/redding/sanford-protocol","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/redding/sanford-protocol","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redding%2Fsanford-protocol","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redding%2Fsanford-protocol/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redding%2Fsanford-protocol/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redding%2Fsanford-protocol/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/redding","download_url":"https://codeload.github.com/redding/sanford-protocol/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redding%2Fsanford-protocol/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278655061,"owners_count":26022966,"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","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["ruby","sanford","tcp","tcp-protocol"],"created_at":"2024-11-05T18:14:15.166Z","updated_at":"2025-10-06T17:54:11.182Z","avatar_url":"https://github.com/redding.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sanford Protocol\n\nRuby implementation of Sanford TCP communication protocol.\n\n## The Protocol\n\n**Version**: `2`\n\nSanford communicates using binary encoded messages.  Sanford messages are two headers and a body:\n\n```\n|------ 1B -------|------ 4B -------|---- (Body Size)B ----|\n| (packed header) | (packed header) | (BSON binary string) |\n|     Version     |    Body Size    |         Body         |\n|-----------------|-----------------|----------------------|\n```\n\n### Version\n\nThe first header represents the protocol version in use.  It is a 1 byte unsigned integer and exists to ensure both the client and the server are talking the same protocol.\n\n### Body Size\n\nThe second header represents the size of the message's body.  It is a 4 byte unsigned integer and tells the receiver how many bytes to read to receive the body.\n\n### Body\n\nThe Body is the content of the message.  It is a [BSON](http://bsonspec.org/) encoded binary string that decodes to a ruby hash.  Since the size of the body is encoded as a 4 byte (32 bit) unsigned integer, there is a size limit for body data (`(2 ** 32) - 1` or `4,294,967,295` or `~4GB`).\n\n## Request\n\nA request is made up of 2 required parts: the name, and the params.\n\n* **name**   - (string) name of the requested API service.\n* **params** - (document) data for the service call - must be a BSON document (ruby Hash, python dict, Javascript Object).\n\nRequests are encoded as BSON hashes when transmitted in messages.\n\n```ruby\n{ 'name'   =\u003e 'some_service',\n  'params' =\u003e { 'key' =\u003e 'value' }\n}\n\nrequest = Sanford::Protocol::Request.parse(a_bson_request_hash)\nrequest.name     #=\u003e \"some_service\"\nrequest.params   #=\u003e { 'key' =\u003e 'value' }\nrequest.to_s     #=\u003e \"some_service\"\n```\n\n## Response\n\nA response is made up of 2 parts: the status and the data.\n\n* **status** - (tuple, required) A code and message describing the result of the service call.\n* **data** - (object, optional) Return value of the service call. This can be any BSON serializable object.  Typically won't be set if the request is not successful.\n\nResponses are encoded as BSON hashes when transmitted in messages.\n\n```ruby\n{ 'status'  =\u003e [ 200, 'The request was successful.' ]\n  'data'    =\u003e true\n}\n\nresponse = Sanford::Protocol::Response.parse(a_bson_response_hash)\nresponse.status.code    #=\u003e 200\nresponse.status.to_i    #=\u003e 200\nresponse.status.name    #=\u003e \"OK\"\nresponse.status.message #=\u003e \"The request was successful.\"\nresponse.status.to_s    #=\u003e \"[200, OK]\"\nresponse.code           #=\u003e 200\nresponse.to_s           #=\u003e \"[200, OK]\"\nresponse.data           #=\u003e true\n```\n\n### Status Codes\n\nThis is the list of defined status codes.\n\n* `200` - `OK`- The request was successful.\n* `400` - `BAD REQUEST` - The request couldn't be read. This is usually because it was not formed correctly.\n* `404` - `NOT FOUND` - The server couldn't find something requested.\n* `408` - `TIMEOUT` - A client connected but didn't write a request before the server timeod out waiting for one.\n* `422` - `INVALID` - The request was sent with invalid params.\n* `500` - `ERROR` - The server errored responding to the request.\n\nIn addition to these, a service can return custom status codes, but they should use a number greater than or equal to `600` to avoid collisions with Sanford's defined status codes.\n\n## Usage\n\nThe `Sanford::Protocol` module defines helper methods for encoding and decoding messages.\n\n```ruby\n# encode a message\ndata = { 'something' =\u003e true }\nmsg_body = Sanford::Protocol.msg_body.encode(data)\nmsg_size = Sanford::Protocol.msg_size.encode(msg_body.bytesize)\nmsg = [Sanford::Protocol.msg_version, msg_size, msg_body].join\n```\n\n### Connection\n\nIf you are sending and receiving messages using a tcp socket, use `Sanford::Protocol::Connection`.\n\n```ruby\nconnection = Sanford::Protocol::Connection.new(tcp_socket)\nincoming_data = connection.read\nconnection.write(outgoing_data)\n```\n\nFor incoming messages, it reads them off the socket, validates them, and returns the decoded body data.  For outgoing messages, it encodes the message body from given data, adds the appropiate message headers, and writes the message to the socket.\n\n#### Timeout\n\nWhen reading data from a connection, you can optionally pass a timeout value.  If given, the connection will block and wait until data is ready to be read.  If a timeout occurs, the connection will raise `TimeoutError`.\n\n```ruby\nbegin\n  connection.read(10)  # timeout after waiting on data for 10s\nrescue Sanford::Protocol::TimeoutError =\u003e err\n  puts \"timeout - so sad :(\"\nend\n```\n\n### Requests And Responses\n\nRequest and response objects have helpers for sending and receiving data using a connection.\n\n```ruby\n# For a server...\n# use Request#parse to build an incoming request\ndata_hash = server_connection.read\nincoming_request = Sanford::Protocol::Request.parse(data_hash)\n# use Response#to_hash to send a response\noutgoing_response = Sanford::Protocol::Response.new(status, data)\nserver_connection.write(outgoing_response.to_hash)\n\n# For a client...\n# use Request#to_hash to send a request\noutgoing_request = Sanford::Protocol::Request.new(name, params)\nclient_connection.write(outgoing_request.to_hash)\n# use Response#parse to build an incoming response\ndata_hash = client_connection.read\nincoming_response = Sanford::Protocol::Response.parse(data_hash)\n```\n\n## Test Helpers\n\nA `FakeSocket` helper class and an associated `TestHelpers` module are provided to help test receiving and sending Sanford::Protocol messages without using real sockets.\n\n```ruby\n# fake a socket with some incoming binary\nsocket = FakeSocket.new(msg_binary_string)\nconnection = Sanford::Protocol::Connection.new(socket)\nmsg_data = connection.read\n\n# write some binary to that fake socket and verify it\nconnection.write(msg_data)\nputs socket.out # =\u003e msg binary string\n\n# create a socket with an incoming request and verify the request\nsocket   = FakeSocket.with_request(*request_params)\nmsg_data = Sanford::Protocol::Connection.new(socket).read\nrequest  = Sanford::Protocol::Request.parse(msg_data)\n```\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredding%2Fsanford-protocol","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fredding%2Fsanford-protocol","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredding%2Fsanford-protocol/lists"}