├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gobycontract.go └── gobycontract_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - "1.10" 6 | - 1.11.x 7 | - master -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Junade 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go By Contract 2 | 3 | This is a simple implementation of production-safe Design by Contract in Go, using pre-conditions and post-conditions. 4 | 5 | Whilst this implementation allows for errors to "fail hard" in development and staging environments, it also can be 6 | configured in a report-only mode for production environments. 7 | 8 | This is an example of the following pattern: [A Pattern for Validating Design by Contract Assertions in Production (with Go and Sentry)](https://icyapril.com/go/programming/2019/03/25/a-pattern-for-validating-design-by-contract-in-Production-with-go-and-sentry.html) 9 | 10 | ## Environment Variables 11 | 12 | * `GOBYCONTRACT_DONTPANIC` when set to "true" will stop panicking when the ````Require```` or `````Ensure```` checks fail 13 | * `SENTRY_DSN` is required to be set for the Sentry reporting of contract violations to be enabled 14 | 15 | Per the [Sentry Raven-Go](https://docs.sentry.io/clients/go/) documentation, the ````SENTRY_RELEASE```` and 16 | ````SENTRY_ENVIRONMENT```` environment variables may also be set. 17 | 18 | ## Example Implementation 19 | 20 | * Require is used to determine if a precondition is met 21 | * Ensure is used to determine if a postcondition is met 22 | 23 | Example of a function using such methods: 24 | 25 | ```go 26 | func SecondsToSecondsAndMinutes(seconds int) (minutes int, remainingSeconds int) { 27 | gobycontract.Require(seconds >= 0, "Input seconds must be positive") 28 | 29 | minutes = seconds/60 30 | remainingSeconds = seconds % 60 31 | 32 | gobycontract.Ensure(minutes >= 0, "Output minutes must be positive") 33 | gobycontract.Ensure(remainingSeconds >= 0, "Output remaining seconds must be positive") 34 | gobycontract.Ensure(remainingSeconds < 60, "There can be no more than 59 remaining seconds") 35 | 36 | return 37 | } 38 | ``` -------------------------------------------------------------------------------- /gobycontract.go: -------------------------------------------------------------------------------- 1 | // Go By Contract provides a lightweight mechanism to validate Design-by-Contract pre-conditions and post-conditions in 2 | // production environments by using Sentry error reporting instead of "fail hard" behaviour. 3 | // 4 | // This small library demonstrates the pattern described in the following blog post: https://icyapril.com/go/programming/2019/03/25/a-pattern-for-validating-design-by-contract-in-Production-with-go-and-sentry.html 5 | // 6 | // Setting the GOBYCONTRACT_DONTPANIC environment variable disables panics for a production environment when set to 7 | // "true", whilst the SENTRY_DSN environment will independently enable reporting to Sentry. 8 | // 9 | // The following is an example of using Require for preconditions and Ensure for postconditions: 10 | // func SecondsToSecondsAndMinutes(seconds int) (minutes int, remainingSeconds int) { 11 | // gobycontract.Require(seconds >= 0, "Input seconds must be positive") 12 | // 13 | // minutes = seconds/60 14 | // remainingSeconds = seconds % 60 15 | // 16 | // gobycontract.Ensure(minutes >= 0, "Output minutes must be positive") 17 | // gobycontract.Ensure(remainingSeconds >= 0, "Output remaining seconds must be positive") 18 | // gobycontract.Ensure(remainingSeconds < 60, "There can be no more than 59 remaining seconds") 19 | // 20 | // return 21 | // } 22 | package gobycontract 23 | 24 | import ( 25 | "os" 26 | "strings" 27 | "github.com/getsentry/raven-go" 28 | ) 29 | 30 | 31 | func Require(pass bool, description string) { 32 | if pass == true { 33 | return 34 | } 35 | 36 | message := "Pre-Condition not met: " + description 37 | 38 | if shouldPanic() == true { 39 | panic(message) 40 | } 41 | 42 | logToSentry(message, "pre-condition") 43 | } 44 | 45 | func Ensure(pass bool, description string) { 46 | if pass == true { 47 | return 48 | } 49 | 50 | message := "Post-Condition not met: " + description 51 | 52 | if shouldPanic() == true { 53 | panic(message) 54 | } 55 | 56 | logToSentry(message, "post-condition") 57 | } 58 | 59 | func shouldPanic() (shouldPanic bool) { 60 | gbcShouldPanic := os.Getenv("GOBYCONTRACT_DONTPANIC") 61 | gbcShouldPanic = strings.ToLower(gbcShouldPanic) 62 | 63 | shouldPanic = true 64 | if gbcShouldPanic == "true" { 65 | shouldPanic = false 66 | } 67 | 68 | return 69 | } 70 | 71 | func logToSentry(message string, category string) { 72 | shouldLog := len(os.Getenv("SENTRY_DSN")) > 0 73 | 74 | if shouldLog == false { 75 | return 76 | } 77 | 78 | raven.CaptureMessageAndWait(message, map[string]string{"category": "contract"+category}) 79 | 80 | } -------------------------------------------------------------------------------- /gobycontract_test.go: -------------------------------------------------------------------------------- 1 | package gobycontract_test 2 | 3 | import ( 4 | "testing" 5 | "os" 6 | "github.com/IcyApril/gobycontract" 7 | "fmt" 8 | "strconv" 9 | ) 10 | 11 | func ExampleSecondsToSecondsAndMinutes() { 12 | minutes, seconds := SecondsToSecondsAndMinutes(125) 13 | fmt.Println(strconv.Itoa(minutes) + " minutes " + strconv.Itoa(seconds) + " seconds") 14 | // output: 15 | // 2 minutes 5 seconds 16 | } 17 | 18 | func SecondsToSecondsAndMinutes(seconds int) (minutes int, remainingSeconds int) { 19 | gobycontract.Require(seconds >= 0, "Input seconds must be positive") 20 | 21 | minutes = seconds/60 22 | remainingSeconds = seconds % 60 23 | 24 | gobycontract.Ensure(minutes >= 0, "Output minutes must be positive") 25 | gobycontract.Ensure(remainingSeconds >= 0, "Output remaining seconds must be positive") 26 | gobycontract.Ensure(remainingSeconds < 60, "There can be no more than 59 remaining seconds") 27 | 28 | return 29 | } 30 | 31 | func TestSumContract(t *testing.T) { 32 | total := Sum(5, 5) 33 | if total != 10 { 34 | t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) 35 | } 36 | } 37 | 38 | func Sum(a int, b int) (result int) { 39 | gobycontract.Require(a > 0, "Argument a must be > 0") 40 | gobycontract.Require(b > 0, "Argument b must be > 0") 41 | 42 | result = sum(a, b) 43 | 44 | gobycontract.Ensure(result == (a + b), "Return value must be a + b") 45 | 46 | return 47 | } 48 | 49 | func sum(a int, b int) int { 50 | return a + b 51 | } 52 | 53 | func TestBrokenSumContract(t *testing.T) { 54 | defer func() { 55 | if r := recover(); r == nil { 56 | t.Errorf("The code did not panic") 57 | } 58 | }() 59 | 60 | os.Unsetenv("GOBYCONTRACT_DONTPANIC") 61 | 62 | total := BrokenSum(5, 5) 63 | if total != 0 { 64 | t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) 65 | } 66 | } 67 | 68 | func TestBrokenSumContractNoPanic(t *testing.T) { 69 | os.Setenv("GOBYCONTRACT_DONTPANIC", "true") 70 | 71 | total := BrokenSum(5, 5) 72 | if total != 0 { 73 | t.Errorf("Sum was incorrect, got: %d, want: %d.", total, 10) 74 | } 75 | } 76 | 77 | func BrokenSum(a int, b int) (result int) { 78 | gobycontract.Require(a > 0, "Argument a must be > 0") 79 | gobycontract.Require(b > 0, "Argument b must be > 0") 80 | 81 | result = brokenSum(a, b) 82 | 83 | gobycontract.Ensure(result == (a + b), "Return value must be a + b") 84 | 85 | return 86 | } 87 | 88 | func brokenSum(a int, b int) int { 89 | return a - b 90 | } --------------------------------------------------------------------------------