diff --git a/README.md b/README.md index c68cf68..f0cab14 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ -# fcc-learn-golang-assets -A snapshot of the assets for the Learn Go course on FreeCodeCamp's youtube +# Assets for "Learn Go" on FreeCodeCamp + +This is a snapshot of the code samples for the ["Learn Go" course](https://boot.dev/courses/learn-golang) on [Boot.dev](https://boot.dev) at the time the video for FreeCodeCamp was released on YouTube. If you want the most up-to-date version of the code, please visit the official [Boot.dev course](https://boot.dev/courses/learn-http). Otherwise, if you're looking for the files used in the video, you're in the right place! + +* [Course code samples](/course) +* [Project steps](/project) + +## License + +You are free to use this content and code for personal education purposes. However, you are *not* authorized to publish this content or code elsewhere, whether for commercial purposes or not. diff --git a/course/1-intro/exercises/1-learn_to_run_code/code.go b/course/1-intro/exercises/1-learn_to_run_code/code.go new file mode 100644 index 0000000..f36dd60 --- /dev/null +++ b/course/1-intro/exercises/1-learn_to_run_code/code.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // single-line comments start with "//" + // comments are just for documentation - they don't execute + fmt.Println("hello world") +} diff --git a/course/1-intro/exercises/1-learn_to_run_code/complete.go b/course/1-intro/exercises/1-learn_to_run_code/complete.go new file mode 100644 index 0000000..efdd760 --- /dev/null +++ b/course/1-intro/exercises/1-learn_to_run_code/complete.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // single-line comments start with "//" + // comments are just for documentation - they don't execute + fmt.Println("starting Textio server") +} diff --git a/course/1-intro/exercises/1-learn_to_run_code/expected.txt b/course/1-intro/exercises/1-learn_to_run_code/expected.txt new file mode 100644 index 0000000..d39814f --- /dev/null +++ b/course/1-intro/exercises/1-learn_to_run_code/expected.txt @@ -0,0 +1 @@ +starting Textio server diff --git a/course/1-intro/exercises/1-learn_to_run_code/readme.md b/course/1-intro/exercises/1-learn_to_run_code/readme.md new file mode 100644 index 0000000..eb7b66f --- /dev/null +++ b/course/1-intro/exercises/1-learn_to_run_code/readme.md @@ -0,0 +1,11 @@ +# Welcome to "Learn Go" + +*This course assumes you're familiar with programming basics. If you're new to coding check out our [Learn Python course](https://boot.dev/learn/learn-python) first.* + +![golang gopher](https://go.dev/blog/gopher/header.jpg) + +## Booting up the "Textio" server + +All the code you write in this course is part of a larger product: an SMS API called "Textio". Textio sends text messages over the internet, it's basically a cute Twilio clone. + +Assignment: log `starting Textio server` to the console instead of `hello world`. diff --git a/course/1-intro/exercises/2-bug/code.go b/course/1-intro/exercises/2-bug/code.go new file mode 100644 index 0000000..578dbe5 --- /dev/null +++ b/course/1-intro/exercises/2-bug/code.go @@ -0,0 +1,22 @@ +package main + +import "fmt" + +func main() { + messagesFromDoris := []string{ + "You doing anything later??", + "Did you get my last message?", + "Don't leave me hanging...", + "Please respond I'm lonely!", + } + numMessages := float64(len(messagesFromDoris)) + costPerMessage := .02 + + // don't touch above this line + + totalCost := costPerMessage + numMessages + + // don't touch below this line + + fmt.Printf("Doris spent %.2f on text messages today\n", totalCost) +} diff --git a/course/1-intro/exercises/2-bug/complete.go b/course/1-intro/exercises/2-bug/complete.go new file mode 100644 index 0000000..5b1823c --- /dev/null +++ b/course/1-intro/exercises/2-bug/complete.go @@ -0,0 +1,22 @@ +package main + +import "fmt" + +func main() { + messagesFromDoris := []string{ + "You doing anything later??", + "Did you get my last message?", + "Don't leave me hanging...", + "Please respond I'm lonely!", + } + numMessages := float64(len(messagesFromDoris)) + costPerMessage := .02 + + // don't touch above this line + + totalCost := costPerMessage * numMessages + + // don't touch below this line + + fmt.Printf("Doris spent %.2f on text messages today\n", totalCost) +} diff --git a/course/1-intro/exercises/2-bug/expected.txt b/course/1-intro/exercises/2-bug/expected.txt new file mode 100644 index 0000000..5b460f7 --- /dev/null +++ b/course/1-intro/exercises/2-bug/expected.txt @@ -0,0 +1 @@ +Doris spent 0.08 on text messages today diff --git a/course/1-intro/exercises/2-bug/readme.md b/course/1-intro/exercises/2-bug/readme.md new file mode 100644 index 0000000..eb65b19 --- /dev/null +++ b/course/1-intro/exercises/2-bug/readme.md @@ -0,0 +1,5 @@ +# Fix a Bug! + +Textio users are reporting that we're billing them for wildly inaccurate amounts. They're *supposed* to be billed `.02` dollars for each text message sent. + +**Fix the math bug on line 17.** diff --git a/course/1-intro/exercises/3-compiling_xkcd/multiple_choice.json b/course/1-intro/exercises/3-compiling_xkcd/multiple_choice.json new file mode 100644 index 0000000..165fac4 --- /dev/null +++ b/course/1-intro/exercises/3-compiling_xkcd/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Go code generally runs ____ than interpreted languages and compiles ____ than other compiled languages like C and Rust", + "answers": [ + "faster, faster", + "faster, slower", + "slower, faster", + "slower, slower" + ] +} diff --git a/course/1-intro/exercises/3-compiling_xkcd/readme.md b/course/1-intro/exercises/3-compiling_xkcd/readme.md new file mode 100644 index 0000000..511ced8 --- /dev/null +++ b/course/1-intro/exercises/3-compiling_xkcd/readme.md @@ -0,0 +1,11 @@ +# Go is fast, simple and productive + +Generally speaking, compiled languages run much faster than interpreted languages, and Go is no exception. + +Go is one of the fastest programming languages, beating JavaScript, Python, and Ruby handily in most benchmarks. + +However, Go code doesn't *run* quite as fast as its compiled Rust and C counterparts. That said, it *compiles* much faster than they do, which makes the developer experience super productive. Unfortunately, there are no swordfights on Go teams... + +![xkcd compiling](https://imgs.xkcd.com/comics/compiling.png) + +*- comic by [xkcd](https://xkcd.com/303/)* diff --git a/course/1-intro/exercises/4-lang_compare_speed/multiple_choice.json b/course/1-intro/exercises/4-lang_compare_speed/multiple_choice.json new file mode 100644 index 0000000..69d20c5 --- /dev/null +++ b/course/1-intro/exercises/4-lang_compare_speed/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Does Go generally execute faster than Rust?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/1-intro/exercises/4-lang_compare_speed/readme.md b/course/1-intro/exercises/4-lang_compare_speed/readme.md new file mode 100644 index 0000000..256d69e --- /dev/null +++ b/course/1-intro/exercises/4-lang_compare_speed/readme.md @@ -0,0 +1,21 @@ +# Comparing Go's Speed + +Go is *generally* faster and more lightweight than interpreted or VM-powered languages like: + +* Python +* JavaScript +* PHP +* Ruby +* Java + +However, in terms of execution speed, Go does lag behind some other compiled languages like: + +* C +* C++ +* Rust + +Go is a bit slower mostly due to its automated memory management, also known as the "Go runtime". Slightly slower speed is the price we pay for memory safety and simple syntax! + +![speed comparison](https://miro.medium.com/max/2020/1*nlpYI256BR71xMBWd1nlfg.png) + +Textio is an amazing candidate for a Go project. We'll be able to quickly process large amounts of text all while using a language that is safe and simple to write. diff --git a/course/1-intro/exercises/5-compiling_code/code.go b/course/1-intro/exercises/5-compiling_code/code.go new file mode 100644 index 0000000..bd73f6f --- /dev/null +++ b/course/1-intro/exercises/5-compiling_code/code.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("the-compiled Textio server is starting" +} diff --git a/course/1-intro/exercises/5-compiling_code/complete.go b/course/1-intro/exercises/5-compiling_code/complete.go new file mode 100644 index 0000000..ac99b4f --- /dev/null +++ b/course/1-intro/exercises/5-compiling_code/complete.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("the-compiled Textio server is starting") +} diff --git a/course/1-intro/exercises/5-compiling_code/expected.txt b/course/1-intro/exercises/5-compiling_code/expected.txt new file mode 100644 index 0000000..8c95158 --- /dev/null +++ b/course/1-intro/exercises/5-compiling_code/expected.txt @@ -0,0 +1 @@ +the-compiled Textio server is starting diff --git a/course/1-intro/exercises/5-compiling_code/readme.md b/course/1-intro/exercises/5-compiling_code/readme.md new file mode 100644 index 0000000..400ada0 --- /dev/null +++ b/course/1-intro/exercises/5-compiling_code/readme.md @@ -0,0 +1,17 @@ +# The Compilation Process + +Computers need machine code, they don't understand English or even Go. We need to convert our high-level (Go) code into machine language, which is really just a set of instructions that some specific hardware can understand. In your case, your CPU. + +The Go compiler's job is to take Go code and produce machine code. On Windows, that would be a `.exe` file. On Mac or Linux, it would be any executable file. The code you write in your browser here on Boot.dev is being compiled for you on Boot.dev's servers, then the machine code is executed in your browser as [Web Assembly](https://blog.boot.dev/golang/running-go-in-the-browser-with-web-assembly-wasm). + +## A note on the structure of a Go program + +We'll go over this all later in more detail, but to sate your curiosity for now, here are a few tidbits about the code. + +1. `package main` lets the Go compiler know that we want this code to compile and run as a standalone program, as opposed to being a library that's imported by other programs. +2. `import fmt` imports the `fmt` (formatting) package. The formatting package exists in Go's standard library and let's us do things like print text to the console. +3. `func main()` defines the `main` function. `main` is the name of the function that acts as the entry point for a Go program. + +## Assignment + +To pass this exercise, fix the compiler error in the code. diff --git a/course/1-intro/exercises/6-what_is_compiled/multiple_choice.json b/course/1-intro/exercises/6-what_is_compiled/multiple_choice.json new file mode 100644 index 0000000..154fcf8 --- /dev/null +++ b/course/1-intro/exercises/6-what_is_compiled/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Do computer processors understand English instructions like 'open the browser'?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/1-intro/exercises/6-what_is_compiled/readme.md b/course/1-intro/exercises/6-what_is_compiled/readme.md new file mode 100644 index 0000000..e58b46b --- /dev/null +++ b/course/1-intro/exercises/6-what_is_compiled/readme.md @@ -0,0 +1,33 @@ +# Compiling explained + +Computers don't know how to do anything unless we as programmers tell them what to do. + +Unfortunately computers don't understand human language. In fact, they don't even understand uncompiled computer programs. + +For example, the code: + +```go +package main + +import "fmt" + +func main(){ + fmt.Println("hello world") +} +``` + +means *nothing* to a computer. + +## Computers need machine code + +A computer's [CPU](https://en.wikipedia.org/wiki/Central_processing_unit) only understands its own *instruction set*, which we call "machine code". + +Instructions are basic math operations like addition, subtraction, multiplication, and the ability to save data temporarily. + +For example, an [ARM processor](https://en.wikipedia.org/wiki/ARM_architecture) uses the *ADD* instruction when supplied with the number `0100` in binary. + +## Go, C, Rust, etc. + +Go, C, and Rust are all languages where the code is first converted to machine code by the compiler before it's executed. + +![compiler](https://www.astateofdata.com/wp-content/uploads/2019/09/code-compiler-machine-code.png) diff --git a/course/1-intro/exercises/7-lang_compare_compiled/multiple_choice.json b/course/1-intro/exercises/7-lang_compare_compiled/multiple_choice.json new file mode 100644 index 0000000..5d26ad5 --- /dev/null +++ b/course/1-intro/exercises/7-lang_compare_compiled/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Do users of compiled programs need access to source code?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/1-intro/exercises/7-lang_compare_compiled/readme.md b/course/1-intro/exercises/7-lang_compare_compiled/readme.md new file mode 100644 index 0000000..fafe230 --- /dev/null +++ b/course/1-intro/exercises/7-lang_compare_compiled/readme.md @@ -0,0 +1,24 @@ +# Compiled vs Interpreted + +Compiled programs can be run without access to the original source code, and without access to a compiler. For example, when your browser executes the code you write in this course, it doesn't use the original code, just the compiled result. + +Note how this is different than interpreted languages like Python and JavaScript. With Python and JavaScript the code is interpreted at [runtime](https://en.wikipedia.org/wiki/Runtime_(program_lifecycle_phase)) by a separate program known as the "interpreter". Distributing code for users to run can be a pain because they need to have an interpreter installed, and they need access to the original source code. + +## Examples of compiled languages + +* Go +* C +* C++ +* Rust + +## Examples of interpreted languages + +* JavaScript +* Python +* Ruby + +![compiled vs interpreted](https://i.imgur.com/ovHaWmS.jpg) + +## Why build Textio in a compiled language? + +One of the most convenient things about using a compiled language like Go for Textio is that when we deploy our server we don't need to include any runtime language dependencies like Node or a Python interpreter. We just add the pre-compiled binary to the server and start it up! diff --git a/course/1-intro/exercises/7a-lang_compare_compiled/multiple_choice.json b/course/1-intro/exercises/7a-lang_compare_compiled/multiple_choice.json new file mode 100644 index 0000000..0685307 --- /dev/null +++ b/course/1-intro/exercises/7a-lang_compare_compiled/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Which language is interpreted?", + "answers": [ + "Python", + "Rust", + "Go", + "C++" + ] +} diff --git a/course/1-intro/exercises/7a-lang_compare_compiled/readme.md b/course/1-intro/exercises/7a-lang_compare_compiled/readme.md new file mode 100644 index 0000000..a842fb8 --- /dev/null +++ b/course/1-intro/exercises/7a-lang_compare_compiled/readme.md @@ -0,0 +1,24 @@ +# Compiled vs Interpreted + +Compiled programs can be run without access to the original source code, and without access to a compiler. For example, when your browser executes the code you write in this course, it doesn't use the original code, just the compiled result. + +Note how this is different than interpreted languages like Python and JavaScript. With Python and JavaScript the code is interpreted at [runtime](https://en.wikipedia.org/wiki/Runtime_(program_lifecycle_phase)) by a separate program known as the "interpreter". Distributing code for users to run can be a pain because they need to have an interpreter installed, and they need access to the original source code. + +## Examples of compiled languages + +* Go +* C +* C++ +* Rust + +## Examples of interpreted languages + +* JavaScript +* Python +* Ruby + +![compiled vs interpreted](https://i.imgur.com/ovHaWmS.jpg) + +## Deploying compiled programs is generally more simple + +One of the most convenient things about using a compiled language like Go for Textio is that when we deploy our server we don't need to include any runtime language dependencies like Node or a Python interpreter. We just add the pre-compiled binary to the server and start it up! diff --git a/course/1-intro/exercises/7b-lang_compare_compiled/multiple_choice.json b/course/1-intro/exercises/7b-lang_compare_compiled/multiple_choice.json new file mode 100644 index 0000000..7edcbd0 --- /dev/null +++ b/course/1-intro/exercises/7b-lang_compare_compiled/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Why is it generally more simple to deploy a compiled server program?", + "answers": [ + "There are no runtime language dependencies", + "Compiled code is faster", + "Compiled code is more memory efficient", + "Because docker exists" + ] +} diff --git a/course/1-intro/exercises/7b-lang_compare_compiled/readme.md b/course/1-intro/exercises/7b-lang_compare_compiled/readme.md new file mode 100644 index 0000000..fafe230 --- /dev/null +++ b/course/1-intro/exercises/7b-lang_compare_compiled/readme.md @@ -0,0 +1,24 @@ +# Compiled vs Interpreted + +Compiled programs can be run without access to the original source code, and without access to a compiler. For example, when your browser executes the code you write in this course, it doesn't use the original code, just the compiled result. + +Note how this is different than interpreted languages like Python and JavaScript. With Python and JavaScript the code is interpreted at [runtime](https://en.wikipedia.org/wiki/Runtime_(program_lifecycle_phase)) by a separate program known as the "interpreter". Distributing code for users to run can be a pain because they need to have an interpreter installed, and they need access to the original source code. + +## Examples of compiled languages + +* Go +* C +* C++ +* Rust + +## Examples of interpreted languages + +* JavaScript +* Python +* Ruby + +![compiled vs interpreted](https://i.imgur.com/ovHaWmS.jpg) + +## Why build Textio in a compiled language? + +One of the most convenient things about using a compiled language like Go for Textio is that when we deploy our server we don't need to include any runtime language dependencies like Node or a Python interpreter. We just add the pre-compiled binary to the server and start it up! diff --git a/course/1-intro/exercises/8-strongly_typed/code.go b/course/1-intro/exercises/8-strongly_typed/code.go new file mode 100644 index 0000000..be690b3 --- /dev/null +++ b/course/1-intro/exercises/8-strongly_typed/code.go @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + var username string = "wagslane" + var password int = 20947382822 + + // don't edit below this line + fmt.Println("Authorization: Basic", username+":"+password) +} diff --git a/course/1-intro/exercises/8-strongly_typed/complete.go b/course/1-intro/exercises/8-strongly_typed/complete.go new file mode 100644 index 0000000..4111c51 --- /dev/null +++ b/course/1-intro/exercises/8-strongly_typed/complete.go @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + var username string = "wagslane" + var password string = "20947382822" + + // don't edit below this line + fmt.Println("Authorization: Basic", username+":"+password) +} diff --git a/course/1-intro/exercises/8-strongly_typed/expected.txt b/course/1-intro/exercises/8-strongly_typed/expected.txt new file mode 100644 index 0000000..139ac08 --- /dev/null +++ b/course/1-intro/exercises/8-strongly_typed/expected.txt @@ -0,0 +1 @@ +Authorization: Basic wagslane:20947382822 diff --git a/course/1-intro/exercises/8-strongly_typed/readme.md b/course/1-intro/exercises/8-strongly_typed/readme.md new file mode 100644 index 0000000..082c466 --- /dev/null +++ b/course/1-intro/exercises/8-strongly_typed/readme.md @@ -0,0 +1,17 @@ +# Go is Strongly Typed + +Go enforces strong and static typing, meaning variables can only have a single type. A `string` variable like "hello world" can not be changed to an `int`, such as the number `3`. + +One of the biggest benefits of strong typing is that errors can be caught at "compile time". In other words, bugs are more easily caught ahead of time because they are detected when the code is compiled before it even runs. + +Contrast this with most interpreted languages, where the variable types are dynamic. Dynamic typing can lead to subtle bugs that are hard to detect. With interpreted languages, the code *must* be run (sometimes in production if you are unlucky 😨) to catch syntax and type errors. + +## Concatenating strings + +Two strings can be [concatenated](https://en.wikipedia.org/wiki/Concatenation) with the `+` operator. Because Go is strongly typed, it won't allow you to concatenate a string variable with a numeric variable. + +## Assignment + +We'll be using simple [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) for the Textio API. This is how our users will communicate to us who they are and how many features they are paying for with each request to our API. + +The code on the right has a type error. Change the type of `password` to a string (but use the same numeric value) so that it can be concatenated with the `username` variable. diff --git a/course/1-intro/exercises/9-lang_compare_memory/multiple_choice.json b/course/1-intro/exercises/9-lang_compare_memory/multiple_choice.json new file mode 100644 index 0000000..bc1372c --- /dev/null +++ b/course/1-intro/exercises/9-lang_compare_memory/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Generally speaking, which language uses more memory?", + "answers": [ + "Java", + "Go" + ] +} diff --git a/course/1-intro/exercises/9-lang_compare_memory/readme.md b/course/1-intro/exercises/9-lang_compare_memory/readme.md new file mode 100644 index 0000000..b0c0e9e --- /dev/null +++ b/course/1-intro/exercises/9-lang_compare_memory/readme.md @@ -0,0 +1,17 @@ +# Go programs are easy on memory + +Go programs are fairly lightweight. Each program includes a small amount of "extra" code that's included in the executable binary. This extra code is called the [Go Runtime](https://go.dev/doc/faq#runtime). One of the purposes of the Go runtime is to cleanup unused memory at runtime. + +In other words, the Go compiler includes a small amount of extra logic in every Go program to make it easier for developers to write code that's memory efficient. + +## Comparison + +As a general rule Java programs use *more* memory than comparable Go programs because Go doesn't use an entire virtual machine to run its programs, just a small runtime. The Go runtime is small enough that it is included directly in each Go program's compiled machine code. + +As another general rule Rust and C++ programs use slightly *less* memory than Go programs because more control is given to the developer to optimize memory usage of the program. The Go runtime just handles it for us automatically. + +## Idle memory usage + +![idle memory](https://miro.medium.com/max/1400/1*Ggs-bJxobwZmrbfuoWGpFw.png) + +In the chart above, [Dexter Darwich compares the memory usage](https://medium.com/@dexterdarwich/comparison-between-java-go-and-rust-fdb21bd5fb7c) of three *very* simple programs written in Java, Go, and Rust. As you can see, Go and Rust use *very* little memory when compared to Java. diff --git a/course/1-intro/exercises/9a-lang_compare_memory/multiple_choice.json b/course/1-intro/exercises/9a-lang_compare_memory/multiple_choice.json new file mode 100644 index 0000000..27c16c4 --- /dev/null +++ b/course/1-intro/exercises/9a-lang_compare_memory/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What's one of the purposes of the Go runtime?", + "answers": [ + "To cleanup unused memory", + "To compile Go code", + "To style Go code and make it easier to read", + "To cook fried chicken" + ] +} diff --git a/course/1-intro/exercises/9a-lang_compare_memory/readme.md b/course/1-intro/exercises/9a-lang_compare_memory/readme.md new file mode 100644 index 0000000..b0c0e9e --- /dev/null +++ b/course/1-intro/exercises/9a-lang_compare_memory/readme.md @@ -0,0 +1,17 @@ +# Go programs are easy on memory + +Go programs are fairly lightweight. Each program includes a small amount of "extra" code that's included in the executable binary. This extra code is called the [Go Runtime](https://go.dev/doc/faq#runtime). One of the purposes of the Go runtime is to cleanup unused memory at runtime. + +In other words, the Go compiler includes a small amount of extra logic in every Go program to make it easier for developers to write code that's memory efficient. + +## Comparison + +As a general rule Java programs use *more* memory than comparable Go programs because Go doesn't use an entire virtual machine to run its programs, just a small runtime. The Go runtime is small enough that it is included directly in each Go program's compiled machine code. + +As another general rule Rust and C++ programs use slightly *less* memory than Go programs because more control is given to the developer to optimize memory usage of the program. The Go runtime just handles it for us automatically. + +## Idle memory usage + +![idle memory](https://miro.medium.com/max/1400/1*Ggs-bJxobwZmrbfuoWGpFw.png) + +In the chart above, [Dexter Darwich compares the memory usage](https://medium.com/@dexterdarwich/comparison-between-java-go-and-rust-fdb21bd5fb7c) of three *very* simple programs written in Java, Go, and Rust. As you can see, Go and Rust use *very* little memory when compared to Java. diff --git a/course/10-advanced_functions/exercises/1-higher-order/code.go b/course/10-advanced_functions/exercises/1-higher-order/code.go new file mode 100644 index 0000000..6d26c57 --- /dev/null +++ b/course/10-advanced_functions/exercises/1-higher-order/code.go @@ -0,0 +1,49 @@ +package main + +import "fmt" + +func getFormattedMessages(messages []string, formatter func) []string { + formattedMessages := []string{} + for _, message := range messages { + formattedMessages = append(formattedMessages, formatter(message)) + } + return formattedMessages +} + +// don't touch below this line + +func addSignature(message string) string { + return message + " Kind regards." +} + +func addGreeting(message string) string { + return "Hello! " + message +} + +func test(messages []string, formatter func(string) string) { + defer fmt.Println("====================================") + formattedMessages := getFormattedMessages(messages, formatter) + if len(formattedMessages) != len(messages) { + fmt.Println("The number of messages returned is incorrect.") + return + } + for i, message := range messages { + formatted := formattedMessages[i] + fmt.Printf(" * %s -> %s\n", message, formatted) + } +} + +func main() { + test([]string{ + "Thanks for getting back to me.", + "Great to see you again.", + "I would love to hang out this weekend.", + "Got any hot stock tips?", + }, addSignature) + test([]string{ + "Thanks for getting back to me.", + "Great to see you again.", + "I would love to hang out this weekend.", + "Got any hot stock tips?", + }, addGreeting) +} diff --git a/course/10-advanced_functions/exercises/1-higher-order/complete.go b/course/10-advanced_functions/exercises/1-higher-order/complete.go new file mode 100644 index 0000000..c871dd8 --- /dev/null +++ b/course/10-advanced_functions/exercises/1-higher-order/complete.go @@ -0,0 +1,49 @@ +package main + +import "fmt" + +func getFormattedMessages(messages []string, formatter func(string) string) []string { + formattedMessages := []string{} + for _, message := range messages { + formattedMessages = append(formattedMessages, formatter(message)) + } + return formattedMessages +} + +// don't touch below this line + +func addSignature(message string) string { + return message + " Kind regards." +} + +func addGreeting(message string) string { + return "Hello! " + message +} + +func test(messages []string, formatter func(string) string) { + defer fmt.Println("====================================") + formattedMessages := getFormattedMessages(messages, formatter) + if len(formattedMessages) != len(messages) { + fmt.Println("The number of messages returned is incorrect.") + return + } + for i, message := range messages { + formatted := formattedMessages[i] + fmt.Printf(" * %s -> %s\n", message, formatted) + } +} + +func main() { + test([]string{ + "Thanks for getting back to me.", + "Great to see you again.", + "I would love to hang out this weekend.", + "Got any hot stock tips?", + }, addSignature) + test([]string{ + "Thanks for getting back to me.", + "Great to see you again.", + "I would love to hang out this weekend.", + "Got any hot stock tips?", + }, addGreeting) +} diff --git a/course/10-advanced_functions/exercises/1-higher-order/expected.txt b/course/10-advanced_functions/exercises/1-higher-order/expected.txt new file mode 100644 index 0000000..6e26c2d --- /dev/null +++ b/course/10-advanced_functions/exercises/1-higher-order/expected.txt @@ -0,0 +1,10 @@ + * Thanks for getting back to me. -> Thanks for getting back to me. Kind regards. + * Great to see you again. -> Great to see you again. Kind regards. + * I would love to hang out this weekend. -> I would love to hang out this weekend. Kind regards. + * Got any hot stock tips? -> Got any hot stock tips? Kind regards. +==================================== + * Thanks for getting back to me. -> Hello! Thanks for getting back to me. + * Great to see you again. -> Hello! Great to see you again. + * I would love to hang out this weekend. -> Hello! I would love to hang out this weekend. + * Got any hot stock tips? -> Hello! Got any hot stock tips? +==================================== diff --git a/course/10-advanced_functions/exercises/1-higher-order/readme.md b/course/10-advanced_functions/exercises/1-higher-order/readme.md new file mode 100644 index 0000000..8435796 --- /dev/null +++ b/course/10-advanced_functions/exercises/1-higher-order/readme.md @@ -0,0 +1,37 @@ +# First Class and Higher Order Functions + +A programming language is said to have "first-class functions" when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable. + +A function that returns a function or accepts a function as input is called a Higher-Order Function. + +Go supports [first-class](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function) and higher-order functions. Another way to think of this is that a function is just another type -- just like `int`s and `string`s and `bool`s. + +For example, to accept a function as a parameter: + +```go +func add(x, y int) int { + return x + y +} + +func mul(x, y int) int { + return x * y +} + +// aggregate applies the given math function to the first 3 inputs +func aggregate(a, b, c int, arithmetic func(int, int) int) int { + return arithmetic(arithmetic(a, b), c) +} + +func main(){ + fmt.Println(aggregate(2,3,4, add)) + // prints 9 + fmt.Println(aggregate(2,3,4, mul)) + // prints 24 +} +``` + +## Assignment + +Textio is launching a new email messaging product, "Mailio"! + +Fix the compile-time bug in the `getFormattedMessages` function. The function body is correct, but the function signature is not. diff --git a/course/10-advanced_functions/exercises/2-higher_order_quiz/multiple_choice.json b/course/10-advanced_functions/exercises/2-higher_order_quiz/multiple_choice.json new file mode 100644 index 0000000..cf6f3ca --- /dev/null +++ b/course/10-advanced_functions/exercises/2-higher_order_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What is a higher-order function?", + "answers": [ + "A function that takes another function as an argument", + "A function with superior logic", + "A function that is first in the call stack" + ] +} diff --git a/course/10-advanced_functions/exercises/2-higher_order_quiz/readme.md b/course/10-advanced_functions/exercises/2-higher_order_quiz/readme.md new file mode 100644 index 0000000..2ffcc13 --- /dev/null +++ b/course/10-advanced_functions/exercises/2-higher_order_quiz/readme.md @@ -0,0 +1,29 @@ +# Why First-class and Higher-Order Functions? + +At first, it may seem like dynamically creating functions and passing them around as variables adds unnecessary complexity. Most of the time you would be right. There are cases however when functions as values make a lot of sense. Some of these include: + +* [HTTP API](https://en.wikipedia.org/wiki/Web_API) handlers +* [Pub/Sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) handlers +* Onclick callbacks + +Any time you need to run custom code at *a time in the future*, functions as values might make sense. + +## Definition: First-class Functions + +A first-class function is a function that can be treated like any other value. Go supports first-class functions. A function's type is dependent on the types of its parameters and return values. For example, these are different function types: + +```go +func() int +``` + +```go +func(string) int +``` + +## Definition: Higher-Order Functions + +A higher-order function is a function that takes a function as an argument or returns a function as a return value. Go supports higher-order functions. For example, this function takes a function as an argument: + +```go +func aggregate(a, b, c int, arithmetic func(int, int) int) int +``` diff --git a/course/10-advanced_functions/exercises/2a-higher_order_quiz/multiple_choice.json b/course/10-advanced_functions/exercises/2a-higher_order_quiz/multiple_choice.json new file mode 100644 index 0000000..3f023c0 --- /dev/null +++ b/course/10-advanced_functions/exercises/2a-higher_order_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What is a first-class function?", + "answers": [ + "A function that is treated like any other variable", + "A function that has been deemed most important by the architect", + "A function that takes another function as an argument" + ] +} diff --git a/course/10-advanced_functions/exercises/2a-higher_order_quiz/readme.md b/course/10-advanced_functions/exercises/2a-higher_order_quiz/readme.md new file mode 100644 index 0000000..2ffcc13 --- /dev/null +++ b/course/10-advanced_functions/exercises/2a-higher_order_quiz/readme.md @@ -0,0 +1,29 @@ +# Why First-class and Higher-Order Functions? + +At first, it may seem like dynamically creating functions and passing them around as variables adds unnecessary complexity. Most of the time you would be right. There are cases however when functions as values make a lot of sense. Some of these include: + +* [HTTP API](https://en.wikipedia.org/wiki/Web_API) handlers +* [Pub/Sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) handlers +* Onclick callbacks + +Any time you need to run custom code at *a time in the future*, functions as values might make sense. + +## Definition: First-class Functions + +A first-class function is a function that can be treated like any other value. Go supports first-class functions. A function's type is dependent on the types of its parameters and return values. For example, these are different function types: + +```go +func() int +``` + +```go +func(string) int +``` + +## Definition: Higher-Order Functions + +A higher-order function is a function that takes a function as an argument or returns a function as a return value. Go supports higher-order functions. For example, this function takes a function as an argument: + +```go +func aggregate(a, b, c int, arithmetic func(int, int) int) int +``` diff --git a/course/10-advanced_functions/exercises/3-currying/code.go b/course/10-advanced_functions/exercises/3-currying/code.go new file mode 100644 index 0000000..dfa5bec --- /dev/null +++ b/course/10-advanced_functions/exercises/3-currying/code.go @@ -0,0 +1,47 @@ +package main + +import ( + "errors" + "fmt" +) + +// getLogger takes a function that formats two strings into +// a single string and returns a function that formats two strings but prints +// the result instead of returning it +func getLogger(formatter func(string, string) string) func(string, string) { + // ? +} + +// don't touch below this line + +func test(first string, errors []error, formatter func(string, string) string) { + defer fmt.Println("====================================") + logger := getLogger(formatter) + fmt.Println("Logs:") + for _, err := range errors { + logger(first, err.Error()) + } +} + +func colonDelimit(first, second string) string { + return first + ": " + second +} +func commaDelimit(first, second string) string { + return first + ", " + second +} + +func main() { + dbErrors := []error{ + errors.New("out of memory"), + errors.New("cpu is pegged"), + errors.New("networking issue"), + errors.New("invalid syntax"), + } + test("Error on database server", dbErrors, colonDelimit) + + mailErrors := []error{ + errors.New("email too large"), + errors.New("non alphanumeric symbols found"), + } + test("Error on mail server", mailErrors, commaDelimit) +} diff --git a/course/10-advanced_functions/exercises/3-currying/complete.go b/course/10-advanced_functions/exercises/3-currying/complete.go new file mode 100644 index 0000000..7456e5a --- /dev/null +++ b/course/10-advanced_functions/exercises/3-currying/complete.go @@ -0,0 +1,49 @@ +package main + +import ( + "errors" + "fmt" +) + +// getLogger takes a function that formats two strings into +// a single string and returns a function that formats two strings but prints +// the result instead of returning it +func getLogger(formatter func(string, string) string) func(string, string) { + return func(first, second string) { + fmt.Println(formatter(first, second)) + } +} + +// don't touch below this line + +func test(first string, errors []error, formatter func(string, string) string) { + defer fmt.Println("====================================") + logger := getLogger(formatter) + fmt.Println("Logs:") + for _, err := range errors { + logger(first, err.Error()) + } +} + +func colonDelimit(first, second string) string { + return first + ": " + second +} +func commaDelimit(first, second string) string { + return first + ", " + second +} + +func main() { + dbErrors := []error{ + errors.New("out of memory"), + errors.New("cpu is pegged"), + errors.New("networking issue"), + errors.New("invalid syntax"), + } + test("Error on database server", dbErrors, colonDelimit) + + mailErrors := []error{ + errors.New("email too large"), + errors.New("non alphanumeric symbols found"), + } + test("Error on mail server", mailErrors, commaDelimit) +} diff --git a/course/10-advanced_functions/exercises/3-currying/expected.txt b/course/10-advanced_functions/exercises/3-currying/expected.txt new file mode 100644 index 0000000..aa927a7 --- /dev/null +++ b/course/10-advanced_functions/exercises/3-currying/expected.txt @@ -0,0 +1,10 @@ +Logs: +Error on database server: out of memory +Error on database server: cpu is pegged +Error on database server: networking issue +Error on database server: invalid syntax +==================================== +Logs: +Error on mail server, email too large +Error on mail server, non alphanumeric symbols found +==================================== diff --git a/course/10-advanced_functions/exercises/3-currying/readme.md b/course/10-advanced_functions/exercises/3-currying/readme.md new file mode 100644 index 0000000..8734347 --- /dev/null +++ b/course/10-advanced_functions/exercises/3-currying/readme.md @@ -0,0 +1,40 @@ +# Currying + +Function currying is the practice of writing a function that takes a function (or functions) as input, and returns a new function. + +For example: + +```go +func main() { + squareFunc := selfMath(multiply) + doubleFunc := selfMath(add) + + fmt.Println(squareFunc(5)) + // prints 25 + + fmt.Println(doubleFunc(5)) + // prints 10 +} + +func multiply(x, y int) int { + return x * y +} + +func add(x, y int) int { + return x + y +} + +func selfMath(mathFunc func(int, int) int) func (int) int { + return func(x int) int { + return mathFunc(x, x) + } +} +``` + +In the example above, the `selfMath` function takes in a function as its parameter, and returns a function that itself returns the value of running that input function on its parameter. + +## Assignment + +The Mailio API needs a very robust error-logging system so we can see when things are going awry in the back-end system. We need a function that can create a custom "logger" (a function that prints to the console) given a specific formatter. + +Complete the `getLogger` function. It should `return` *a new function* that prints the formatted inputs using the given `formatter` function. The inputs should be passed into the formatter function in the order they are given to the logger function. diff --git a/course/10-advanced_functions/exercises/4-defer/code.go b/course/10-advanced_functions/exercises/4-defer/code.go new file mode 100644 index 0000000..c3e0416 --- /dev/null +++ b/course/10-advanced_functions/exercises/4-defer/code.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "sort" +) + +const ( + logDeleted = "user deleted" + logNotFound = "user not found" + logAdmin = "admin deleted" +) + +func logAndDelete(users map[string]user, name string) (log string) { + user, ok := users[name] + if !ok { + delete(users, name) + return logNotFound + } + if user.admin { + return logAdmin + } + delete(users, name) + return logDeleted +} + +// don't touch below this line + +type user struct { + name string + number int + admin bool +} + +func test(users map[string]user, name string) { + fmt.Printf("Attempting to delete %s...\n", name) + defer fmt.Println("====================================") + log := logAndDelete(users, name) + fmt.Println("Log:", log) +} + +func main() { + users := map[string]user{ + "john": { + name: "john", + number: 18965554631, + admin: true, + }, + "elon": { + name: "elon", + number: 19875556452, + admin: true, + }, + "breanna": { + name: "breanna", + number: 98575554231, + admin: false, + }, + "kade": { + name: "kade", + number: 10765557221, + admin: false, + }, + } + + fmt.Println("Initial users:") + usersSorted := []string{} + for name := range users { + usersSorted = append(usersSorted, name) + } + sort.Strings(usersSorted) + for _, name := range usersSorted { + fmt.Println(" -", name) + } + fmt.Println("====================================") + + test(users, "john") + test(users, "santa") + test(users, "kade") + + fmt.Println("Final users:") + usersSorted = []string{} + for name := range users { + usersSorted = append(usersSorted, name) + } + sort.Strings(usersSorted) + for _, name := range usersSorted { + fmt.Println(" -", name) + } + fmt.Println("====================================") +} diff --git a/course/10-advanced_functions/exercises/4-defer/complete.go b/course/10-advanced_functions/exercises/4-defer/complete.go new file mode 100644 index 0000000..ebdfb0c --- /dev/null +++ b/course/10-advanced_functions/exercises/4-defer/complete.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "sort" +) + +const ( + logDeleted = "user deleted" + logNotFound = "user not found" + logAdmin = "admin deleted" +) + +func logAndDelete(users map[string]user, name string) (log string) { + defer delete(users, name) + + user, ok := users[name] + if !ok { + return logNotFound + } + if user.admin { + return logAdmin + } + return logDeleted +} + +// don't touch below this line + +type user struct { + name string + number int + admin bool +} + +func test(users map[string]user, name string) { + fmt.Printf("Attempting to delete %s...\n", name) + defer fmt.Println("====================================") + log := logAndDelete(users, name) + fmt.Println("Log:", log) +} + +func main() { + users := map[string]user{ + "john": { + name: "john", + number: 18965554631, + admin: true, + }, + "elon": { + name: "elon", + number: 19875556452, + admin: true, + }, + "breanna": { + name: "breanna", + number: 98575554231, + admin: false, + }, + "kade": { + name: "kade", + number: 10765557221, + admin: false, + }, + } + + fmt.Println("Initial users:") + usersSorted := []string{} + for name := range users { + usersSorted = append(usersSorted, name) + } + sort.Strings(usersSorted) + for _, name := range usersSorted { + fmt.Println(" -", name) + } + fmt.Println("====================================") + + test(users, "john") + test(users, "santa") + test(users, "kade") + + fmt.Println("Final users:") + usersSorted = []string{} + for name := range users { + usersSorted = append(usersSorted, name) + } + sort.Strings(usersSorted) + for _, name := range usersSorted { + fmt.Println(" -", name) + } + fmt.Println("====================================") +} diff --git a/course/10-advanced_functions/exercises/4-defer/expected.txt b/course/10-advanced_functions/exercises/4-defer/expected.txt new file mode 100644 index 0000000..81ec423 --- /dev/null +++ b/course/10-advanced_functions/exercises/4-defer/expected.txt @@ -0,0 +1,19 @@ +Initial users: + - breanna + - elon + - john + - kade +==================================== +Attempting to delete john... +Log: admin deleted +==================================== +Attempting to delete santa... +Log: user not found +==================================== +Attempting to delete kade... +Log: user deleted +==================================== +Final users: + - breanna + - elon +==================================== diff --git a/course/10-advanced_functions/exercises/4-defer/readme.md b/course/10-advanced_functions/exercises/4-defer/readme.md new file mode 100644 index 0000000..cad05e5 --- /dev/null +++ b/course/10-advanced_functions/exercises/4-defer/readme.md @@ -0,0 +1,46 @@ +# Defer + +The `defer` keyword is a fairly unique feature of Go. It allows a function to be executed automatically *just before* its enclosing function returns. + +The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns. + +Deferred functions are typically used to close database connections, file handlers and the like. + +For example: + +```go +// CopyFile copies a file from srcName to dstName on the local filesystem. +func CopyFile(dstName, srcName string) (written int64, err error) { + + // Open the source file + src, err := os.Open(srcName) + if err != nil { + return + } + // Close the source file when the CopyFile function returns + defer src.Close() + + // Create the destination file + dst, err := os.Create(dstName) + if err != nil { + return + } + // Close the destination file when the CopyFile function returns + defer dst.Close() + + return io.Copy(dst, src) +} +``` + +In the above example, the `src.Close()` function is not called until after the `CopyFile` function was called but immediately before the `CopyFile` function returns. + +Defer is a great way to **make sure** that something happens at the end of a function, even if there are multiple return statements. + +## Assignment + +There is a bug in the `logAndDelete` function, fix it! + +This function should *always* delete the user from the user's map, which is a map that stores the user's name as keys. It also returns a `log` string that indicates to the caller some information about the user's deletion. + +To avoid bugs like this in the future, instead of calling `delete` before each `return`, just `defer` the delete once at the beginning of the function. + diff --git a/course/10-advanced_functions/exercises/5-closures/code.go b/course/10-advanced_functions/exercises/5-closures/code.go new file mode 100644 index 0000000..a815131 --- /dev/null +++ b/course/10-advanced_functions/exercises/5-closures/code.go @@ -0,0 +1,46 @@ +package main + +import "fmt" + +func adder() func(int) int { + // ? +} + +// don't touch below this line + +type emailBill struct { + costInPennies int +} + +func test(bills []emailBill) { + defer fmt.Println("====================================") + countAdder, costAdder := adder(), adder() + for _, bill := range bills { + fmt.Printf("You've sent %d emails and it has cost you %d cents\n", countAdder(1), costAdder(bill.costInPennies)) + } +} + +func main() { + test([]emailBill{ + {45}, + {32}, + {43}, + {12}, + {34}, + {54}, + }) + + test([]emailBill{ + {12}, + {12}, + {976}, + {12}, + {543}, + }) + + test([]emailBill{ + {743}, + {13}, + {8}, + }) +} diff --git a/course/10-advanced_functions/exercises/5-closures/complete.go b/course/10-advanced_functions/exercises/5-closures/complete.go new file mode 100644 index 0000000..55df6bc --- /dev/null +++ b/course/10-advanced_functions/exercises/5-closures/complete.go @@ -0,0 +1,50 @@ +package main + +import "fmt" + +func adder() func(int) int { + sum := 0 + return func(x int) int { + sum += x + return sum + } +} + +// don't touch below this line + +type emailBill struct { + costInPennies int +} + +func test(bills []emailBill) { + defer fmt.Println("====================================") + countAdder, costAdder := adder(), adder() + for _, bill := range bills { + fmt.Printf("You've sent %d emails and it has cost you %d cents\n", countAdder(1), costAdder(bill.costInPennies)) + } +} + +func main() { + test([]emailBill{ + {45}, + {32}, + {43}, + {12}, + {34}, + {54}, + }) + + test([]emailBill{ + {12}, + {12}, + {976}, + {12}, + {543}, + }) + + test([]emailBill{ + {743}, + {13}, + {8}, + }) +} diff --git a/course/10-advanced_functions/exercises/5-closures/expected.txt b/course/10-advanced_functions/exercises/5-closures/expected.txt new file mode 100644 index 0000000..537dc58 --- /dev/null +++ b/course/10-advanced_functions/exercises/5-closures/expected.txt @@ -0,0 +1,17 @@ +You've sent 1 emails and it has cost you 45 cents +You've sent 2 emails and it has cost you 77 cents +You've sent 3 emails and it has cost you 120 cents +You've sent 4 emails and it has cost you 132 cents +You've sent 5 emails and it has cost you 166 cents +You've sent 6 emails and it has cost you 220 cents +==================================== +You've sent 1 emails and it has cost you 12 cents +You've sent 2 emails and it has cost you 24 cents +You've sent 3 emails and it has cost you 1000 cents +You've sent 4 emails and it has cost you 1012 cents +You've sent 5 emails and it has cost you 1555 cents +==================================== +You've sent 1 emails and it has cost you 743 cents +You've sent 2 emails and it has cost you 756 cents +You've sent 3 emails and it has cost you 764 cents +==================================== diff --git a/course/10-advanced_functions/exercises/5-closures/readme.md b/course/10-advanced_functions/exercises/5-closures/readme.md new file mode 100644 index 0000000..505dc57 --- /dev/null +++ b/course/10-advanced_functions/exercises/5-closures/readme.md @@ -0,0 +1,37 @@ +# Closures + +A closure is a function that references variables from outside its own function body. The function may access and *assign* to the referenced variables. + +In this example, the `concatter()` function returns a function that has reference to an *enclosed* `doc` value. Each successive call to `harryPotterAggregator` mutates that same `doc` variable. + +```go +func concatter() func(string) string { + doc := "" + return func(word string) string { + doc += word + " " + return doc + } +} + +func main() { + harryPotterAggregator := concatter() + harryPotterAggregator("Mr.") + harryPotterAggregator("and") + harryPotterAggregator("Mrs.") + harryPotterAggregator("Dursley") + harryPotterAggregator("of") + harryPotterAggregator("number") + harryPotterAggregator("four,") + harryPotterAggregator("Privet") + + fmt.Println(harryPotterAggregator("Drive")) + // Mr. and Mrs. Dursley of number four, Privet Drive +} +``` + +## Assignment + +Keeping track of how many emails we send is mission-critical at Mailio. Complete the `adder()` function. + +It should return a function that adds its input (an `int`) to an enclosed `sum` value, then return the new sum. In other words, it keeps a running total of the `sum` variable within a closure. + diff --git a/course/10-advanced_functions/exercises/6-clousures_quiz/multiple_choice.json b/course/10-advanced_functions/exercises/6-clousures_quiz/multiple_choice.json new file mode 100644 index 0000000..a17ba57 --- /dev/null +++ b/course/10-advanced_functions/exercises/6-clousures_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Can a closure mutate a variable outside its body?", + "answers": [ + "Yes", + "No" + ] +} diff --git a/course/10-advanced_functions/exercises/6-clousures_quiz/readme.md b/course/10-advanced_functions/exercises/6-clousures_quiz/readme.md new file mode 100644 index 0000000..e49779d --- /dev/null +++ b/course/10-advanced_functions/exercises/6-clousures_quiz/readme.md @@ -0,0 +1,30 @@ +# Closure Review + +A closure is a function that references variables from outside its own function body. The function may access and *assign* to the referenced variables. + +## Example Closure + +```go +func concatter() func(string) string { + doc := "" + return func(word string) string { + doc += word + " " + return doc + } +} + +func main() { + harryPotterAggregator := concatter() + harryPotterAggregator("Mr.") + harryPotterAggregator("and") + harryPotterAggregator("Mrs.") + harryPotterAggregator("Dursley") + harryPotterAggregator("of") + harryPotterAggregator("number") + harryPotterAggregator("four,") + harryPotterAggregator("Privet") + + fmt.Println(harryPotterAggregator("Drive")) + // Mr. and Mrs. Dursley of number four, Privet Drive +} +``` diff --git a/course/10-advanced_functions/exercises/6a-clousures_quiz/multiple_choice.json b/course/10-advanced_functions/exercises/6a-clousures_quiz/multiple_choice.json new file mode 100644 index 0000000..191b33e --- /dev/null +++ b/course/10-advanced_functions/exercises/6a-clousures_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "When a variable is enclosed in a closure, the enclosing function has access to ____", + "answers": [ + "a mutable reference to the original value", + "a copy of the value" + ] +} diff --git a/course/10-advanced_functions/exercises/6a-clousures_quiz/readme.md b/course/10-advanced_functions/exercises/6a-clousures_quiz/readme.md new file mode 100644 index 0000000..e49779d --- /dev/null +++ b/course/10-advanced_functions/exercises/6a-clousures_quiz/readme.md @@ -0,0 +1,30 @@ +# Closure Review + +A closure is a function that references variables from outside its own function body. The function may access and *assign* to the referenced variables. + +## Example Closure + +```go +func concatter() func(string) string { + doc := "" + return func(word string) string { + doc += word + " " + return doc + } +} + +func main() { + harryPotterAggregator := concatter() + harryPotterAggregator("Mr.") + harryPotterAggregator("and") + harryPotterAggregator("Mrs.") + harryPotterAggregator("Dursley") + harryPotterAggregator("of") + harryPotterAggregator("number") + harryPotterAggregator("four,") + harryPotterAggregator("Privet") + + fmt.Println(harryPotterAggregator("Drive")) + // Mr. and Mrs. Dursley of number four, Privet Drive +} +``` diff --git a/course/10-advanced_functions/exercises/7-anonymous_functions/code.go b/course/10-advanced_functions/exercises/7-anonymous_functions/code.go new file mode 100644 index 0000000..91ecec8 --- /dev/null +++ b/course/10-advanced_functions/exercises/7-anonymous_functions/code.go @@ -0,0 +1,36 @@ +package main + +import "fmt" + +func printReports(messages []string) { + // ? +} + +// don't touch below this line + +func test(messages []string) { + defer fmt.Println("====================================") + printReports(messages) +} + +func main() { + test([]string{ + "Here's Johnny!", + "Go ahead, make my day", + "You had me at hello", + "There's no place like home", + }) + + test([]string{ + "Hello, my name is Inigo Montoya. You killed my father. Prepare to die.", + "May the Force be with you.", + "Show me the money!", + "Go ahead, make my day.", + }) +} + +func printCostReport(costCalculator func(string) int, message string) { + cost := costCalculator(message) + fmt.Printf(`Message: "%s" Cost: %v cents`, message, cost) + fmt.Println() +} diff --git a/course/10-advanced_functions/exercises/7-anonymous_functions/complete.go b/course/10-advanced_functions/exercises/7-anonymous_functions/complete.go new file mode 100644 index 0000000..32aaa6b --- /dev/null +++ b/course/10-advanced_functions/exercises/7-anonymous_functions/complete.go @@ -0,0 +1,40 @@ +package main + +import "fmt" + +func printReports(messages []string) { + for _, message := range messages { + printCostReport(func(m string) int { + return len(m) * 2 + }, message) + } +} + +// don't touch below this line + +func test(messages []string) { + defer fmt.Println("====================================") + printReports(messages) +} + +func main() { + test([]string{ + "Here's Johnny!", + "Go ahead, make my day", + "You had me at hello", + "There's no place like home", + }) + + test([]string{ + "Hello, my name is Inigo Montoya. You killed my father. Prepare to die.", + "May the Force be with you.", + "Show me the money!", + "Go ahead, make my day.", + }) +} + +func printCostReport(costCalculator func(string) int, message string) { + cost := costCalculator(message) + fmt.Printf(`Message: "%s" Cost: %v cents`, message, cost) + fmt.Println() +} diff --git a/course/10-advanced_functions/exercises/7-anonymous_functions/expected.txt b/course/10-advanced_functions/exercises/7-anonymous_functions/expected.txt new file mode 100644 index 0000000..2fc54a9 --- /dev/null +++ b/course/10-advanced_functions/exercises/7-anonymous_functions/expected.txt @@ -0,0 +1,10 @@ +Message: "Here's Johnny!" Cost: 28 cents +Message: "Go ahead, make my day" Cost: 42 cents +Message: "You had me at hello" Cost: 38 cents +Message: "There's no place like home" Cost: 52 cents +==================================== +Message: "Hello, my name is Inigo Montoya. You killed my father. Prepare to die." Cost: 140 cents +Message: "May the Force be with you." Cost: 52 cents +Message: "Show me the money!" Cost: 36 cents +Message: "Go ahead, make my day." Cost: 44 cents +==================================== diff --git a/course/10-advanced_functions/exercises/7-anonymous_functions/readme.md b/course/10-advanced_functions/exercises/7-anonymous_functions/readme.md new file mode 100644 index 0000000..325a3af --- /dev/null +++ b/course/10-advanced_functions/exercises/7-anonymous_functions/readme.md @@ -0,0 +1,38 @@ +# Anonymous Functions + +Anonymous functions are true to form in that they have *no name*. We've been using them throughout this chapter, but we haven't really talked about them yet. + +Anonymous functions are useful when defining a function that will only be used once or to create a quick [closure](https://en.wikipedia.org/wiki/Closure_(computer_programming)). + +```go +// doMath accepts a function that converts one int into another +// and a slice of ints. It returns a slice of ints that have been +// converted by the passed in function. +func doMath(f func(int) int, nums []int) []int { + var results []int + for _, n := range nums { + results = append(results, f(n)) + } + return results +} + +func main() { + nums := []int{1, 2, 3, 4, 5} + + // Here we define an anonymous function that doubles an int + // and pass it to doMath + allNumsDoubled := doMath(func(x int) int { + return x + x + }, nums) + + fmt.Println(allNumsDoubled) + // prints: + // [2 4 6 8 10] +} +``` + +## Assignment + +Complete the `printReports` function. + +Call `printCostReport` once for each message. Pass in an anonymous function as the `costCalculator` that returns an `int` equal to twice the length of the input message. diff --git a/course/11-pointers/exercises/1-pointers_intro/code.go b/course/11-pointers/exercises/1-pointers_intro/code.go new file mode 100644 index 0000000..f51d540 --- /dev/null +++ b/course/11-pointers/exercises/1-pointers_intro/code.go @@ -0,0 +1,29 @@ +package main + +import "fmt" + +type Message struct { + Recipient string + Text string +} + +// Don't touch above this line + +func sendMessage(m Message) { + fmt.Printf("To: %v\n", &m.Recipient) + fmt.Printf("Message: %v\n", &m.Text) +} + +// Don't touch below this line + +func test(recipient string, text string) { + m := Message{Recipient: recipient, Text: text} + sendMessage(m) + fmt.Println("=====================================") +} + +func main() { + test("Lane", "Textio is getting better everyday!") + test("Allan", "This pointer stuff is weird...") + test("Tiffany", "What time will you be home for dinner?") +} diff --git a/course/11-pointers/exercises/1-pointers_intro/complete.go b/course/11-pointers/exercises/1-pointers_intro/complete.go new file mode 100644 index 0000000..205cf5b --- /dev/null +++ b/course/11-pointers/exercises/1-pointers_intro/complete.go @@ -0,0 +1,29 @@ +package main + +import "fmt" + +type Message struct { + Recipient string + Text string +} + +// Don't touch above this line + +func sendMessage(m Message) { + fmt.Printf("To: %v\n", m.Recipient) + fmt.Printf("Message: %v\n", m.Text) +} + +// Don't touch below this line + +func test(recipient string, text string) { + m := Message{Recipient: recipient, Text: text} + sendMessage(m) + fmt.Println("=====================================") +} + +func main() { + test("Lane", "Textio is getting better everyday!") + test("Allan", "This pointer stuff is weird...") + test("Tiffany", "What time will you be home for dinner?") +} diff --git a/course/11-pointers/exercises/1-pointers_intro/expected.txt b/course/11-pointers/exercises/1-pointers_intro/expected.txt new file mode 100755 index 0000000..c821ccd --- /dev/null +++ b/course/11-pointers/exercises/1-pointers_intro/expected.txt @@ -0,0 +1,9 @@ +To: Lane +Message: Textio is getting better everyday! +===================================== +To: Allan +Message: This pointer stuff is weird... +===================================== +To: Tiffany +Message: What time will you be home for dinner? +===================================== diff --git a/course/11-pointers/exercises/1-pointers_intro/readme.md b/course/11-pointers/exercises/1-pointers_intro/readme.md new file mode 100644 index 0000000..cf65672 --- /dev/null +++ b/course/11-pointers/exercises/1-pointers_intro/readme.md @@ -0,0 +1,33 @@ +# Introduction to Pointers + +As we have learned, a variable is a named location in memory that stores a value. We can manipulate the value of a variable by assigning a new value to it or by performing operations on it. When we assign a value to a variable, we are storing that value in a specific location in memory. + +```go +x := 42 +// "x" is the name of a location in memory. That location is storing the integer value of 42 +``` + +## A pointer is a variable + +A pointer is a variable that stores the *memory address* of another variable. This means that a pointer "points to" the *location* of where the data is stored *NOT* the actual data itself. + +The `*` syntax defines a pointer: + +```go +var p *int +``` + +The `&` operator generates a pointer to its operand. + +```go +myString := "hello" +myStringPtr = &myString +``` + +## Why are pointers useful? + +Pointers allow us to manipulate data in memory directly, without making copies or duplicating data. This can make programs more efficient and allow us to do things that would be difficult or impossible without them. + +## Assignment + +Fix the bug in the `sendMessage` function. It's *supposed* to print a nicely formatted message to the console containing an SMS's recipient and message body. However, it's not working as expected. Run the code and see what happens, then fix the bug. diff --git a/course/11-pointers/exercises/2-pointers_practice/code.go b/course/11-pointers/exercises/2-pointers_practice/code.go new file mode 100644 index 0000000..9b69934 --- /dev/null +++ b/course/11-pointers/exercises/2-pointers_practice/code.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" +) + +func removeProfanity(message *string) { + // ? +} + +// don't touch below this line + +func test(messages []string) { + for _, message := range messages { + removeProfanity(&message) + fmt.Println(message) + } +} + +func main() { + messages1 := []string{ + "well shoot, this is awful", + "dang robots", + "dang them to heck", + } + + messages2 := []string{ + "well shoot", + "Allan is going straight to heck", + "dang... that's a tough break", + } + + test(messages1) + test(messages2) +} diff --git a/course/11-pointers/exercises/2-pointers_practice/complete.go b/course/11-pointers/exercises/2-pointers_practice/complete.go new file mode 100644 index 0000000..1663fca --- /dev/null +++ b/course/11-pointers/exercises/2-pointers_practice/complete.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "strings" +) + +func removeProfanity(message *string) { + messageVal := *message + messageVal = strings.ReplaceAll(messageVal, "dang", "****") + messageVal = strings.ReplaceAll(messageVal, "shoot", "*****") + messageVal = strings.ReplaceAll(messageVal, "heck", "****") + *message = messageVal +} + +// don't touch below this line + +func test(messages []string) { + for _, message := range messages { + removeProfanity(&message) + fmt.Println(message) + } +} + +func main() { + messages1 := []string{ + "well shoot, this is awful", + "dang robots", + "dang them to heck", + } + + messages2 := []string{ + "well shoot", + "Allan is going straight to heck", + "dang... that's a tough break", + } + + test(messages1) + test(messages2) +} diff --git a/course/11-pointers/exercises/2-pointers_practice/expected.txt b/course/11-pointers/exercises/2-pointers_practice/expected.txt new file mode 100644 index 0000000..dc6ac90 --- /dev/null +++ b/course/11-pointers/exercises/2-pointers_practice/expected.txt @@ -0,0 +1,6 @@ +well *****, this is awful +**** robots +**** them to **** +well ***** +Allan is going straight to **** +****... that's a tough break diff --git a/course/11-pointers/exercises/2-pointers_practice/readme.md b/course/11-pointers/exercises/2-pointers_practice/readme.md new file mode 100644 index 0000000..26f1e9b --- /dev/null +++ b/course/11-pointers/exercises/2-pointers_practice/readme.md @@ -0,0 +1,45 @@ +# Pointers + +Pointers hold the memory address of a value. + +The `*` syntax defines a pointer: + +```go +var p *int +``` + +A pointer's zero value is `nil` + +The & operator generates a pointer to its operand. + +```go +myString := "hello" +myStringPtr = &myString +``` + +The * dereferences a pointer to gain access to the value + +```go +fmt.Println(*myStringPtr) // read myString through the pointer +*myStringPtr = "world" // set myString through the pointer +``` + +Unlike C, Go has no [pointer arithmetic](https://www.tutorialspoint.com/cprogramming/c_pointer_arithmetic.htm) + +## Just because you can doesn't mean you should + +We're doing this exercise to understand that pointers **can** be used in this way. That said, pointers can be *very* dangerous. It's generally a better idea to have your functions accept non-pointers and return new values rather than mutating pointer inputs. + +## Assignment + +Complete the `removeProfanity` function. + +It should use the [strings.ReplaceAll](https://pkg.go.dev/strings#ReplaceAll) function to replace all instances of the following words in the input `message` with asterisks. + +* "dang" -> "****" +* "shoot" -> "*****" +* "heck" -> "****" + +It should *mutate* the value in the pointer and return nothing. + +Do *not* alter the function signature. diff --git a/course/11-pointers/exercises/3-pointers_quiz/multiple_choice.json b/course/11-pointers/exercises/3-pointers_quiz/multiple_choice.json new file mode 100644 index 0000000..cd97c53 --- /dev/null +++ b/course/11-pointers/exercises/3-pointers_quiz/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What is the value of *y after the code on the left executes?", + "answers": [ + "100", + "a memory address pointing to x", + "nil", + "50" + ] +} diff --git a/course/11-pointers/exercises/3-pointers_quiz/readme.md b/course/11-pointers/exercises/3-pointers_quiz/readme.md new file mode 100644 index 0000000..6d7cbf4 --- /dev/null +++ b/course/11-pointers/exercises/3-pointers_quiz/readme.md @@ -0,0 +1,13 @@ +# Pointers Quiz + +```go +package main + +import "fmt" + +func main() { + var x int = 50 + var y *int = &x + *y = 100 +} +``` diff --git a/course/11-pointers/exercises/3b-pointers_quiz/multiple_choice.json b/course/11-pointers/exercises/3b-pointers_quiz/multiple_choice.json new file mode 100644 index 0000000..2ffedfd --- /dev/null +++ b/course/11-pointers/exercises/3b-pointers_quiz/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What is the value of x after the code on the left executes?", + "answers": [ + "100", + "a memory address pointing to x", + "50", + "nil" + ] +} diff --git a/course/11-pointers/exercises/3b-pointers_quiz/readme.md b/course/11-pointers/exercises/3b-pointers_quiz/readme.md new file mode 100644 index 0000000..6d7cbf4 --- /dev/null +++ b/course/11-pointers/exercises/3b-pointers_quiz/readme.md @@ -0,0 +1,13 @@ +# Pointers Quiz + +```go +package main + +import "fmt" + +func main() { + var x int = 50 + var y *int = &x + *y = 100 +} +``` diff --git a/course/11-pointers/exercises/4-nil_dereference/code.go b/course/11-pointers/exercises/4-nil_dereference/code.go new file mode 100644 index 0000000..695b7a0 --- /dev/null +++ b/course/11-pointers/exercises/4-nil_dereference/code.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "strings" +) + +func removeProfanity(message *string) { + // ? + messageVal := *message + messageVal = strings.ReplaceAll(messageVal, "dang", "****") + messageVal = strings.ReplaceAll(messageVal, "shoot", "*****") + messageVal = strings.ReplaceAll(messageVal, "heck", "****") + *message = messageVal +} + +// don't touch below this line + +func test(messages []string) { + for _, message := range messages { + if message == "" { + removeProfanity(nil) + fmt.Println("nil message detected") + } else { + removeProfanity(&message) + fmt.Println(message) + } + } +} + +func main() { + messages := []string{ + "well shoot, this is awful", + "", + "dang robots", + "dang them to heck", + "", + } + + messages2 := []string{ + "well shoot", + "", + "Allan is going straight to heck", + "dang... that's a tough break", + "", + } + + test(messages) + test(messages2) + +} diff --git a/course/11-pointers/exercises/4-nil_dereference/complete.go b/course/11-pointers/exercises/4-nil_dereference/complete.go new file mode 100644 index 0000000..c4de32e --- /dev/null +++ b/course/11-pointers/exercises/4-nil_dereference/complete.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "strings" +) + +func removeProfanity(message *string) { + if message == nil { + return + } + messageVal := *message + messageVal = strings.ReplaceAll(messageVal, "dang", "****") + messageVal = strings.ReplaceAll(messageVal, "shoot", "*****") + messageVal = strings.ReplaceAll(messageVal, "heck", "****") + *message = messageVal +} + +// don't touch below this line + +func test(messages []string) { + for _, message := range messages { + if message == "" { + removeProfanity(nil) + fmt.Println("nil message detected") + } else { + removeProfanity(&message) + fmt.Println(message) + } + } +} + +func main() { + messages := []string{ + "well shoot, this is awful", + "", + "dang robots", + "dang them to heck", + "", + } + + messages2 := []string{ + "well shoot", + "", + "Allan is going straight to heck", + "dang... that's a tough break", + "", + } + + test(messages) + test(messages2) + +} diff --git a/course/11-pointers/exercises/4-nil_dereference/expected.txt b/course/11-pointers/exercises/4-nil_dereference/expected.txt new file mode 100644 index 0000000..603f277 --- /dev/null +++ b/course/11-pointers/exercises/4-nil_dereference/expected.txt @@ -0,0 +1,10 @@ +well *****, this is awful +nil message detected +**** robots +**** them to **** +nil message detected +well ***** +nil message detected +Allan is going straight to **** +****... that's a tough break +nil message detected diff --git a/course/11-pointers/exercises/4-nil_dereference/readme.md b/course/11-pointers/exercises/4-nil_dereference/readme.md new file mode 100644 index 0000000..cbb98a1 --- /dev/null +++ b/course/11-pointers/exercises/4-nil_dereference/readme.md @@ -0,0 +1,9 @@ +# Nil Pointers + +Pointers can be very dangerous. + +If a pointer points to nothing (the zero value of the pointer type) then dereferencing it will cause a runtime error (a [panic](https://gobyexample.com/panic)) that crashes the program. Generally speaking, whenever you're dealing with pointers you should check if it's `nil` before trying to dereference it. + +## Assignment + +Let's make our profanity checker *safe*. Update the `removeProfanity` function. If `message` is `nil`, `return` early to avoid a [panic](https://gobyexample.com/panic). After all, there are no bad words to remove. diff --git a/course/11-pointers/exercises/5-pointer_receiver/multiple_choice.json b/course/11-pointers/exercises/5-pointer_receiver/multiple_choice.json new file mode 100644 index 0000000..82f2bfe --- /dev/null +++ b/course/11-pointers/exercises/5-pointer_receiver/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which is more widely used in Go?", + "answers": [ + "Pointer receivers", + "Value receivers" + ] +} diff --git a/course/11-pointers/exercises/5-pointer_receiver/readme.md b/course/11-pointers/exercises/5-pointer_receiver/readme.md new file mode 100644 index 0000000..46b7df8 --- /dev/null +++ b/course/11-pointers/exercises/5-pointer_receiver/readme.md @@ -0,0 +1,47 @@ +# Pointer Receivers + +A receiver type on a method can be a pointer. + +Methods with pointer receivers can modify the value to which the receiver points. Since methods often need to modify their receiver, pointer receivers are *more common* than value receivers. + +## Pointer receiver + +```go +type car struct { + color string +} + +func (c *car) setColor(color string) { + c.color = color +} + +func main() { + c := car{ + color: "white", + } + c.setColor("blue") + fmt.Println(c.color) + // prints "blue" +} +``` + +## Non-pointer receiver + +```go +type car struct { + color string +} + +func (c car) setColor(color string) { + c.color = color +} + +func main() { + c := car{ + color: "white", + } + c.setColor("blue") + fmt.Println(c.color) + // prints "white" +} +``` diff --git a/course/11-pointers/exercises/6-pointer_receiver_code/code.go b/course/11-pointers/exercises/6-pointer_receiver_code/code.go new file mode 100644 index 0000000..3c348e2 --- /dev/null +++ b/course/11-pointers/exercises/6-pointer_receiver_code/code.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" +) + +func (e email) setMessage(newMessage string) { + e.message = newMessage +} + +// don't edit below this line + +type email struct { + message string + fromAddress string + toAddress string +} + +func test(e *email, newMessage string) { + fmt.Println("-- before --") + e.print() + fmt.Println("-- end before --") + e.setMessage("this is my second draft") + fmt.Println("-- after --") + e.print() + fmt.Println("-- end after --") + fmt.Println("==========================") +} + +func (e email) print() { + fmt.Println("message:", e.message) + fmt.Println("fromAddress:", e.fromAddress) + fmt.Println("toAddress:", e.toAddress) +} + +func main() { + test(&email{ + message: "this is my first draft", + fromAddress: "sandra@mailio-test.com", + toAddress: "bullock@mailio-test.com", + }, "this is my second draft") + + test(&email{ + message: "this is my third draft", + fromAddress: "sandra@mailio-test.com", + toAddress: "bullock@mailio-test.com", + }, "this is my fourth draft") + +} diff --git a/course/11-pointers/exercises/6-pointer_receiver_code/complete.go b/course/11-pointers/exercises/6-pointer_receiver_code/complete.go new file mode 100644 index 0000000..333f24b --- /dev/null +++ b/course/11-pointers/exercises/6-pointer_receiver_code/complete.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" +) + +func (e *email) setMessage(newMessage string) { + e.message = newMessage +} + +// don't edit below this line + +type email struct { + message string + fromAddress string + toAddress string +} + +func test(e *email, newMessage string) { + fmt.Println("-- before --") + e.print() + fmt.Println("-- end before --") + e.setMessage("this is my second draft") + fmt.Println("-- after --") + e.print() + fmt.Println("-- end after --") + fmt.Println("==========================") +} + +func (e email) print() { + fmt.Println("message:", e.message) + fmt.Println("fromAddress:", e.fromAddress) + fmt.Println("toAddress:", e.toAddress) +} + +func main() { + test(&email{ + message: "this is my first draft", + fromAddress: "sandra@mailio-test.com", + toAddress: "bullock@mailio-test.com", + }, "this is my second draft") + + test(&email{ + message: "this is my third draft", + fromAddress: "sandra@mailio-test.com", + toAddress: "bullock@mailio-test.com", + }, "this is my fourth draft") + +} diff --git a/course/11-pointers/exercises/6-pointer_receiver_code/expected.txt b/course/11-pointers/exercises/6-pointer_receiver_code/expected.txt new file mode 100644 index 0000000..8110ab7 --- /dev/null +++ b/course/11-pointers/exercises/6-pointer_receiver_code/expected.txt @@ -0,0 +1,22 @@ +-- before -- +message: this is my first draft +fromAddress: sandra@mailio-test.com +toAddress: bullock@mailio-test.com +-- end before -- +-- after -- +message: this is my second draft +fromAddress: sandra@mailio-test.com +toAddress: bullock@mailio-test.com +-- end after -- +========================== +-- before -- +message: this is my third draft +fromAddress: sandra@mailio-test.com +toAddress: bullock@mailio-test.com +-- end before -- +-- after -- +message: this is my second draft +fromAddress: sandra@mailio-test.com +toAddress: bullock@mailio-test.com +-- end after -- +========================== diff --git a/course/11-pointers/exercises/6-pointer_receiver_code/readme.md b/course/11-pointers/exercises/6-pointer_receiver_code/readme.md new file mode 100644 index 0000000..7986c57 --- /dev/null +++ b/course/11-pointers/exercises/6-pointer_receiver_code/readme.md @@ -0,0 +1,33 @@ +# Pointer Receiver Code + +Methods with pointer receivers don't require that a pointer is used to call the method. The pointer will automatically be derived from the value. + +```go +type circle struct { + x int + y int + radius int +} + +func (c *circle) grow(){ + c.radius *= 2 +} + +func main(){ + c := circle{ + x: 1, + y: 2, + radius: 4, + } + + // notice c is not a pointer in the calling function + // but the method still gains access to a pointer to c + c.grow() + fmt.Println(c.radius) + // prints 8 +} +``` + +## Assignment + +Fix the bug in the code so that `setMessage` sets the `message` field of the given email structure, and the new value persists outside the scope of the `setMessage` method. diff --git a/course/12-local_development/exercises/1-intro/code.go b/course/12-local_development/exercises/1-intro/code.go new file mode 100644 index 0000000..4b226c6 --- /dev/null +++ b/course/12-local_development/exercises/1-intro/code.go @@ -0,0 +1,14 @@ +package mailio + +import ( + "fmt" +) + +func test(text string) { + fmt.Println(text) +} + +func main() { + test("starting Mailio server") + test("stopping Mailio server") +} diff --git a/course/12-local_development/exercises/1-intro/complete.go b/course/12-local_development/exercises/1-intro/complete.go new file mode 100644 index 0000000..8bb1a0a --- /dev/null +++ b/course/12-local_development/exercises/1-intro/complete.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" +) + +func test(text string) { + fmt.Println(text) +} + +func main() { + test("starting Mailio server") + test("stopping Mailio server") +} diff --git a/course/12-local_development/exercises/1-intro/expected.txt b/course/12-local_development/exercises/1-intro/expected.txt new file mode 100644 index 0000000..0c6414d --- /dev/null +++ b/course/12-local_development/exercises/1-intro/expected.txt @@ -0,0 +1,2 @@ +starting Mailio server +stopping Mailio server diff --git a/course/12-local_development/exercises/1-intro/readme.md b/course/12-local_development/exercises/1-intro/readme.md new file mode 100644 index 0000000..0f87d69 --- /dev/null +++ b/course/12-local_development/exercises/1-intro/readme.md @@ -0,0 +1,28 @@ +# Packages + +Every Go program is made up of packages. + +You have probably noticed the `package main` at the top of all the programs you have been writing. + +A package named "main" has an entrypoint at the `main()` function. A `main` package is compiled into an executable program. + +A package by any other name is a "library package". Libraries have no entry point. Libraries simply export functionality that can be used by other packages. For example: + +```go +package main + +import ( + "fmt" + "math/rand" +) + +func main() { + fmt.Println("My favorite number is", rand.Intn(10)) +} +``` + +This program is an executable. It is a "main" package and *imports* from the `fmt` and `math/rand` library packages. + +## Assignment + +Fix the bug in the code. diff --git a/course/12-local_development/exercises/10-go_install/multiple_choice.json b/course/12-local_development/exercises/10-go_install/multiple_choice.json new file mode 100644 index 0000000..5db170f --- /dev/null +++ b/course/12-local_development/exercises/10-go_install/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What does 'go install' do?", + "answers": [ + "Compiles and installs the program locally", + "Installs dependencies", + "Saves local code to the remote source control provider" + ] +} diff --git a/course/12-local_development/exercises/10-go_install/readme.md b/course/12-local_development/exercises/10-go_install/readme.md new file mode 100644 index 0000000..37a7c7a --- /dev/null +++ b/course/12-local_development/exercises/10-go_install/readme.md @@ -0,0 +1,27 @@ +# Go Install + +## Build an executable + +Ensure you are in your `hellogo` repo, then run: + +```bash +go install +``` + +Navigate out of your project directory: + +```bash +cd ../ +``` + +Go has installed the `hellogo` program globally. Run it with: + +```bash +hellogo +``` + +## Tip about "not found" + +If you get an error regarding "hellogo not found" it means you probably don't have your Go environment setup properly. Specifically, `go install` is adding your binary to your `GOBIN` directory, but that may not be in your `PATH`. + +You can read more about that here in the [go install docs](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies). diff --git a/course/12-local_development/exercises/10a-go_install/multiple_choice.json b/course/12-local_development/exercises/10a-go_install/multiple_choice.json new file mode 100644 index 0000000..b5644a8 --- /dev/null +++ b/course/12-local_development/exercises/10a-go_install/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Code must be compiled with 'go build' before running 'go install'", + "answers": [ + "False", + "True" + ] +} diff --git a/course/12-local_development/exercises/10a-go_install/readme.md b/course/12-local_development/exercises/10a-go_install/readme.md new file mode 100644 index 0000000..37a7c7a --- /dev/null +++ b/course/12-local_development/exercises/10a-go_install/readme.md @@ -0,0 +1,27 @@ +# Go Install + +## Build an executable + +Ensure you are in your `hellogo` repo, then run: + +```bash +go install +``` + +Navigate out of your project directory: + +```bash +cd ../ +``` + +Go has installed the `hellogo` program globally. Run it with: + +```bash +hellogo +``` + +## Tip about "not found" + +If you get an error regarding "hellogo not found" it means you probably don't have your Go environment setup properly. Specifically, `go install` is adding your binary to your `GOBIN` directory, but that may not be in your `PATH`. + +You can read more about that here in the [go install docs](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies). diff --git a/course/12-local_development/exercises/11-custom_package/multiple_choice.json b/course/12-local_development/exercises/11-custom_package/multiple_choice.json new file mode 100644 index 0000000..031c825 --- /dev/null +++ b/course/12-local_development/exercises/11-custom_package/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "What was the output from 'go build' in the library package", + "answers": [ + "The compiled package is silently saved to the local build cache", + "An executable program" + ] +} diff --git a/course/12-local_development/exercises/11-custom_package/readme.md b/course/12-local_development/exercises/11-custom_package/readme.md new file mode 100644 index 0000000..1836dba --- /dev/null +++ b/course/12-local_development/exercises/11-custom_package/readme.md @@ -0,0 +1,45 @@ +# Custom Package + +Let's write a package to import and use in `hellogo`. + +Create a sibling directory at the same level as the `hellogo` directory: + +```bash +mkdir mystrings +cd mystrings +``` + +Initialize a module: + +```bash +go mod init {REMOTE}/{USERNAME}/mystrings +``` + +Then create a new file `mystrings.go` in that directory and paste the following code: + +```go +// by convention, we name our package the same as the directory +package mystrings + +// Reverse reverses a string left to right +// Notice that we need to capitalize the first letter of the function +// If we don't then we won't be able access this function outside of the +// mystrings package +func Reverse(s string) string { + result := "" + for _, v := range s { + result = string(v) + result + } + return result +} +``` + +Note that there is no `main.go` or `func main()` in this package. + +`go build` won't build an executable from a library package. However, `go build` will still compile the package and save it to our local build cache. It's useful for checking for compile errors. + +Run: + +```bash +go build +``` diff --git a/course/12-local_development/exercises/11a-custom_package/multiple_choice.json b/course/12-local_development/exercises/11a-custom_package/multiple_choice.json new file mode 100644 index 0000000..075adb6 --- /dev/null +++ b/course/12-local_development/exercises/11a-custom_package/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Why is the function 'Reverse()' instead of 'reverse()'?", + "answers": [ + "Lowercase names aren't exported for external use", + "Conventionally uppercase names are used in Go" + ] +} diff --git a/course/12-local_development/exercises/11a-custom_package/readme.md b/course/12-local_development/exercises/11a-custom_package/readme.md new file mode 100644 index 0000000..498c214 --- /dev/null +++ b/course/12-local_development/exercises/11a-custom_package/readme.md @@ -0,0 +1,45 @@ +# Custom Package + +Let's write a package to import and use in `hellogo`. + +Create a sibling directory at the same level as the `hellogo` directory: + +```bash +mkdir mystrings +cd mystrings +``` + +Initialize a module: + +```bash +go mod init {REMOTE}/{USERNAME}/mystrings +``` + +Then create a new file `mystrings.go` in that directory and paste the following code: + +```go +// by convention, we name our package the same as the directory +package mystrings + +// Reverse reverses a string left to right +// Notice that we need to capitlize the first letter of the function +// If we don't then we won't be able access this function outside of the +// mystrings package +func Reverse(s string) string { + result := "" + for _, v := range s { + result = string(v) + result + } + return result +} +``` + +Note that there is no `main.go` or `func main()` in this package. + +`go build` won't build an executable from a library package. However, `go build` will still compile the package and save it to our local build cache. It's useful for checking for compile errors. + +Run: + +```bash +go build +``` diff --git a/course/12-local_development/exercises/11b-custom_package/multiple_choice.json b/course/12-local_development/exercises/11b-custom_package/multiple_choice.json new file mode 100644 index 0000000..de12d6d --- /dev/null +++ b/course/12-local_development/exercises/11b-custom_package/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Does a package in a folder named 'dateparser' need to also be called 'dateparser'", + "answers": [ + "No, but it should by convention", + "Yes", + "No, by convention we should avoid consistent naming" + ] +} diff --git a/course/12-local_development/exercises/11b-custom_package/readme.md b/course/12-local_development/exercises/11b-custom_package/readme.md new file mode 100644 index 0000000..498c214 --- /dev/null +++ b/course/12-local_development/exercises/11b-custom_package/readme.md @@ -0,0 +1,45 @@ +# Custom Package + +Let's write a package to import and use in `hellogo`. + +Create a sibling directory at the same level as the `hellogo` directory: + +```bash +mkdir mystrings +cd mystrings +``` + +Initialize a module: + +```bash +go mod init {REMOTE}/{USERNAME}/mystrings +``` + +Then create a new file `mystrings.go` in that directory and paste the following code: + +```go +// by convention, we name our package the same as the directory +package mystrings + +// Reverse reverses a string left to right +// Notice that we need to capitlize the first letter of the function +// If we don't then we won't be able access this function outside of the +// mystrings package +func Reverse(s string) string { + result := "" + for _, v := range s { + result = string(v) + result + } + return result +} +``` + +Note that there is no `main.go` or `func main()` in this package. + +`go build` won't build an executable from a library package. However, `go build` will still compile the package and save it to our local build cache. It's useful for checking for compile errors. + +Run: + +```bash +go build +``` diff --git a/course/12-local_development/exercises/12-custom_package/multiple_choice.json b/course/12-local_development/exercises/12-custom_package/multiple_choice.json new file mode 100644 index 0000000..4a25861 --- /dev/null +++ b/course/12-local_development/exercises/12-custom_package/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What was printed by the new hellogo program?", + "answers": [ + "dlrow olleh", + "hello world", + "world hello" + ] +} diff --git a/course/12-local_development/exercises/12-custom_package/readme.md b/course/12-local_development/exercises/12-custom_package/readme.md new file mode 100644 index 0000000..51ac12c --- /dev/null +++ b/course/12-local_development/exercises/12-custom_package/readme.md @@ -0,0 +1,40 @@ +# Custom Package Continued + +Let's use our new `mystrings` package in `hellogo` + +Modify hellogo's `main.go` file: + +```go +package main + +import ( + "fmt" + + "{REMOTE}/{USERNAME}/mystrings" +) + +func main() { + fmt.Println(mystrings.Reverse("hello world")) +} +``` + +Don't forget to replace {REMOTE} and {USERNAME} with the values you used before. Then edit hellogo's `go.mod` file to contain the following: + +```go +module example.com/username/hellogo + +go 1.20 + +replace example.com/username/mystrings v0.0.0 => ../mystrings + +require ( + example.com/username/mystrings v0.0.0 +) +``` + +Now build and run the new program: + +```bash +go build +./hellogo +``` diff --git a/course/12-local_development/exercises/12a-custom_package/multiple_choice.json b/course/12-local_development/exercises/12a-custom_package/multiple_choice.json new file mode 100644 index 0000000..38efc55 --- /dev/null +++ b/course/12-local_development/exercises/12a-custom_package/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "How does the go toolchain know where to find the imported code?", + "answers": [ + "We used the 'replace' keyword in go.mod to point it to the relative location of mystrings", + "It downloads it from Google's servers", + "NPM hosts the files publicly", + "It was fetched from Github" + ] +} diff --git a/course/12-local_development/exercises/12a-custom_package/readme.md b/course/12-local_development/exercises/12a-custom_package/readme.md new file mode 100644 index 0000000..51ac12c --- /dev/null +++ b/course/12-local_development/exercises/12a-custom_package/readme.md @@ -0,0 +1,40 @@ +# Custom Package Continued + +Let's use our new `mystrings` package in `hellogo` + +Modify hellogo's `main.go` file: + +```go +package main + +import ( + "fmt" + + "{REMOTE}/{USERNAME}/mystrings" +) + +func main() { + fmt.Println(mystrings.Reverse("hello world")) +} +``` + +Don't forget to replace {REMOTE} and {USERNAME} with the values you used before. Then edit hellogo's `go.mod` file to contain the following: + +```go +module example.com/username/hellogo + +go 1.20 + +replace example.com/username/mystrings v0.0.0 => ../mystrings + +require ( + example.com/username/mystrings v0.0.0 +) +``` + +Now build and run the new program: + +```bash +go build +./hellogo +``` diff --git a/course/12-local_development/exercises/13-remote_package/multiple_choice.json b/course/12-local_development/exercises/13-remote_package/multiple_choice.json new file mode 100644 index 0000000..1999154 --- /dev/null +++ b/course/12-local_development/exercises/13-remote_package/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "How did the Go toolchain know where to download the go-tinytime package?", + "answers": [ + "The module import path is used for remote lookups, e.g. https://github.com/wagslane/go-tinytime", + "The go toolchain has every open-source Go module's location memorized" + ] +} diff --git a/course/12-local_development/exercises/13-remote_package/readme.md b/course/12-local_development/exercises/13-remote_package/readme.md new file mode 100644 index 0000000..5de5a16 --- /dev/null +++ b/course/12-local_development/exercises/13-remote_package/readme.md @@ -0,0 +1,86 @@ +# Remote Packages + +Let's learn how to use an open-source package that's available online. + +## A note on how you should publish modules + +Be aware that using the "replace" keyword like we did in the last assignment *isn't advised*, but can be useful to get up and running quickly. The *proper* way to create and depend on modules is to publish them to a remote repository. When you do that, the "replace keyword can be dropped from the `go.mod`: + +### Bad + +This works for local-only development + +```go +module github.com/wagslane/hellogo + +go 1.20 + +replace github.com/wagslane/mystrings v0.0.0 => ../mystrings + +require ( + github.com/wagslane/mystrings v0.0.0 +) +``` + +### Good + +This works if we publish our modules to a remote location like Github as we should. + +```go +module github.com/wagslane/hellogo + +go 1.20 + +require ( + github.com/wagslane/mystrings v0.0.0 +) +``` + +## Assignment + +First, create a new directory in the same parent directory as `hellogo` and `mystrings` called `datetest`. + +Create `main.go` in `datetest` and add the following code: + +```go +package main + +import ( + "fmt" + "time" + + tinytime "github.com/wagslane/go-tinytime" +) + +func main() { + tt := tinytime.New(1585750374) + + tt = tt.Add(time.Hour * 48) + fmt.Println(tt) +} +``` + +Initialize a module: + +```bash +go mod init {REMOTE}/{USERNAME}/datetest +``` + +Download and install the remote go-tinydate package using `go get`: + +```bash +go get github.com/wagslane/go-tinytime +``` + +Print the contents of your go.mod file to see the changes: + +```bash +cat go.mod +``` + +Compile and run your program: + +```bash +go build +./datetest +``` diff --git a/course/12-local_development/exercises/13a-remote_package/multiple_choice.json b/course/12-local_development/exercises/13a-remote_package/multiple_choice.json new file mode 100644 index 0000000..7b79d7c --- /dev/null +++ b/course/12-local_development/exercises/13a-remote_package/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What was printed after running the new datetest program?", + "answers": [ + "2020-04-03T14:12:54Z", + "2025-04-03T14:12:54Z", + "Year: 2020, Month: 04, Day: 05" + ] +} diff --git a/course/12-local_development/exercises/13a-remote_package/readme.md b/course/12-local_development/exercises/13a-remote_package/readme.md new file mode 100644 index 0000000..598216b --- /dev/null +++ b/course/12-local_development/exercises/13a-remote_package/readme.md @@ -0,0 +1,87 @@ +# Remote Packages + + +Let's learn how to use an open-source package that's available online. + +## A note on how you should publish modules + +Be aware that using the "replace" keyword like we did in the last assignment *isn't advised*, but can be useful to get up and running quickly. The *proper* way to create and depend on modules is to publish them to a remote repository. When you do that, the "replace keyword can be dropped from the `go.mod`: + +### Bad + +This works for local-only development + +```go +module github.com/wagslane/hellogo + +go 1.20 + +replace github.com/wagslane/mystrings v0.0.0 => ../mystrings + +require ( + github.com/wagslane/mystrings v0.0.0 +) +``` + +### Good + +This works if we publish our modules to a remote location like Github as we should. + +```go +module github.com/wagslane/hellogo + +go 1.20 + +require ( + github.com/wagslane/mystrings v0.0.0 +) +``` + +## Assignment + +First, create a new directory in the same parent directory as `hellogo` and `mystrings` called `datetest`. + +Create `main.go` in `datetest` and add the following code: + +```go +package main + +import ( + "fmt" + "time" + + tinytime "github.com/wagslane/go-tinytime" +) + +func main() { + tt := tinytime.New(1585750374) + + tt = tt.Add(time.Hour * 48) + fmt.Println(tt) +} +``` + +Initialize a module: + +```bash +go mod init {REMOTE}/{USERNAME}/datetest +``` + +Download and install the remote go-tinydate package using `go get`: + +```bash +go get github.com/wagslane/go-tinytime +``` + +Print the contents of your go.mod file to see the changes: + +```bash +cat go.mod +``` + +Compile and run your program: + +```bash +go build +./datetest +``` diff --git a/course/12-local_development/exercises/14-library_packages_quiz/multiple_choice.json b/course/12-local_development/exercises/14-library_packages_quiz/multiple_choice.json new file mode 100644 index 0000000..c5d329a --- /dev/null +++ b/course/12-local_development/exercises/14-library_packages_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Should you export code from the main package?", + "answers": [ + "Nope", + "Yup" + ] +} diff --git a/course/12-local_development/exercises/14-library_packages_quiz/readme.md b/course/12-local_development/exercises/14-library_packages_quiz/readme.md new file mode 100644 index 0000000..ade18da --- /dev/null +++ b/course/12-local_development/exercises/14-library_packages_quiz/readme.md @@ -0,0 +1,50 @@ +# Clean Packages + +I’ve often seen, and have been responsible for, throwing code into packages without much thought. I’ve quickly drawn a line in the sand and started putting code into different folders (which in Go are different packages by definition) just for the sake of findability. Learning to properly build small and reusable packages can take your Go career to the next level. + +## Rules Of Thumb + +### 1. Hide internal logic + +If you're familiar with the pillars of OOP, this is a practice in *encapsulation*. + +Oftentimes an application will have complex logic that requires a lot of code. In almost every case the logic that the application cares about can be exposed via an API, and most of the dirty work can be kept within a package. For example, imagine we are building an application that needs to classify images. We could build a package: + +```go +package classifier + +// ClassifyImage classifies images as "hotdog" or "not hotdog" +func ClassifyImage(image []byte) (imageType string) { + return hasHotdogColors(image) && hasHotdogShape(image) +} + +func hasHotdogShape(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} + +func hasHotdogColors(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} +``` + +We create an API by only exposing the function(s) that the application-level needs to know about. All other logic is unexported to keep a clean separation of concerns. The application doesn’t need to know how to classify an image, just the result of the classification. + +### 2. Don’t change APIs + +The unexported functions within a package can and should change often for testing, refactoring, and bug fixing. + +A well-designed library will have a stable API so that users aren’t receiving breaking changes each time they update the package version. In Go, this means not changing exported function’s signatures. + +### 3. Don’t export functions from the main package + +A `main` package isn't a library, there's no need to export functions from it. + +### 4. Packages shouldn't know about dependents + +Perhaps one of the most important and most broken rules is that a package shouldn’t know anything about its dependents. In other words, a package should never have specific knowledge about a particular application that uses it. + +## Further Reading + +You can optionally [read more here](https://blog.boot.dev/golang/how-to-separate-library-packages-in-go/) if you're interested. diff --git a/course/12-local_development/exercises/14a-library_packages_quiz/multiple_choice.json b/course/12-local_development/exercises/14a-library_packages_quiz/multiple_choice.json new file mode 100644 index 0000000..d2f84c2 --- /dev/null +++ b/course/12-local_development/exercises/14a-library_packages_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "When should you NOT export a function, variable, or type?", + "answers": [ + "When the end-user doesn't need to know about it", + "Never, its better to share code!" + ] +} diff --git a/course/12-local_development/exercises/14a-library_packages_quiz/readme.md b/course/12-local_development/exercises/14a-library_packages_quiz/readme.md new file mode 100644 index 0000000..ade18da --- /dev/null +++ b/course/12-local_development/exercises/14a-library_packages_quiz/readme.md @@ -0,0 +1,50 @@ +# Clean Packages + +I’ve often seen, and have been responsible for, throwing code into packages without much thought. I’ve quickly drawn a line in the sand and started putting code into different folders (which in Go are different packages by definition) just for the sake of findability. Learning to properly build small and reusable packages can take your Go career to the next level. + +## Rules Of Thumb + +### 1. Hide internal logic + +If you're familiar with the pillars of OOP, this is a practice in *encapsulation*. + +Oftentimes an application will have complex logic that requires a lot of code. In almost every case the logic that the application cares about can be exposed via an API, and most of the dirty work can be kept within a package. For example, imagine we are building an application that needs to classify images. We could build a package: + +```go +package classifier + +// ClassifyImage classifies images as "hotdog" or "not hotdog" +func ClassifyImage(image []byte) (imageType string) { + return hasHotdogColors(image) && hasHotdogShape(image) +} + +func hasHotdogShape(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} + +func hasHotdogColors(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} +``` + +We create an API by only exposing the function(s) that the application-level needs to know about. All other logic is unexported to keep a clean separation of concerns. The application doesn’t need to know how to classify an image, just the result of the classification. + +### 2. Don’t change APIs + +The unexported functions within a package can and should change often for testing, refactoring, and bug fixing. + +A well-designed library will have a stable API so that users aren’t receiving breaking changes each time they update the package version. In Go, this means not changing exported function’s signatures. + +### 3. Don’t export functions from the main package + +A `main` package isn't a library, there's no need to export functions from it. + +### 4. Packages shouldn't know about dependents + +Perhaps one of the most important and most broken rules is that a package shouldn’t know anything about its dependents. In other words, a package should never have specific knowledge about a particular application that uses it. + +## Further Reading + +You can optionally [read more here](https://blog.boot.dev/golang/how-to-separate-library-packages-in-go/) if you're interested. diff --git a/course/12-local_development/exercises/14b-library_packages_quiz/multiple_choice.json b/course/12-local_development/exercises/14b-library_packages_quiz/multiple_choice.json new file mode 100644 index 0000000..b8f7aef --- /dev/null +++ b/course/12-local_development/exercises/14b-library_packages_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Should you often change a package's exported API?", + "answers": [ + "No, try to keep changes to internal functionality", + "Yes, move fast and break things", + "If the package is 'main' then yes" + ] +} diff --git a/course/12-local_development/exercises/14b-library_packages_quiz/readme.md b/course/12-local_development/exercises/14b-library_packages_quiz/readme.md new file mode 100644 index 0000000..c1ca7cd --- /dev/null +++ b/course/12-local_development/exercises/14b-library_packages_quiz/readme.md @@ -0,0 +1,52 @@ +# Clean Packages + +I’ve often seen, and have been responsible for, throwing code into packages without much thought. I’ve quickly drawn a line in the sand and started putting code into different folders (which in Go are different packages by definition) just for the sake of findability. Learning to properly build small and reusable packages can take your Go career to the next level. + +## Rules Of Thumb + +### 1. Hide internal logic + +If you're familiar with the pillars of OOP, this is a practice in *encapsulation*. + +Oftentimes an application will have complex logic that requires a lot of code. In almost every case the logic that the application cares about can be exposed via an API, and most of the dirty work can be kept within a package. For example, imagine we are building an application that needs to classify images. We could build a package: + +```go +package classifier + +// ClassifyImage classifies images as "hotdog" or "not hotdog" +func ClassifyImage(image []byte) (imageType string) { + return hasHotdogColors(image) && hasHotdogShape(image) +} + +func hasHotdogShape(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} + +func hasHotdogColors(image []byte) bool { + // internal logic that the application doesn't need to know about + return true +} +``` + +We create an API by only exposing the function(s) that the application level needs to know about. All other logic is unexported to keep a clean separation of concerns. The application doesn’t need to know how to classify an image, just the result of the classification. + +### 2. Don’t change APIs + +The unexported functions within a package can and should change often for testing, refactoring, and bug fixing. + +A well-designed library will have a stable API so that users aren’t receive breaking changes each time they update the package version. In Go, this means not changing exported function’s signatures. If you *must* make breaking changes, then you should use [semantic versioning](https://semver.org/) to communicate the changes to your users by bumping the major version. + +For example `v1.0.0` -> `v2.0.0` + +### 3. Don’t export functions from the main package + +A `main` package isn't a library, there's no need to export functions from it. + +### 4. Packages shouldn't know about dependents + +Perhaps one of the most important and most broken rules is that a package shouldn’t know anything about its dependents. In other words, a package should never have specific knowledge about a particular application that uses it. + +## Further Reading + +You can optionally [read more here](https://blog.boot.dev/golang/how-to-separate-library-packages-in-go/) if you're interested. diff --git a/course/12-local_development/exercises/2-package_naming/multiple_choice.json b/course/12-local_development/exercises/2-package_naming/multiple_choice.json new file mode 100644 index 0000000..ad257f2 --- /dev/null +++ b/course/12-local_development/exercises/2-package_naming/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What would be the conventional package name of a package with the path github.com/wagslane/parser?", + "answers": [ + "parser", + "wagslane", + "go-parser", + "github.com" + ] +} diff --git a/course/12-local_development/exercises/2-package_naming/readme.md b/course/12-local_development/exercises/2-package_naming/readme.md new file mode 100644 index 0000000..e8dd6e5 --- /dev/null +++ b/course/12-local_development/exercises/2-package_naming/readme.md @@ -0,0 +1,21 @@ +# Package Naming + +## Naming Convention + +By *convention*, a package's name is the same as the last element of its import path. For instance, the `math/rand` package comprises files that begin with: + +```go +package rand +``` + +That said, package names aren't *required* to match their import path. For example, I could write a new package with the path `github.com/mailio/rand` and name the package `random`: + +```go +package random +``` + +While the above is possible, it is discouraged for the sake of consistency. + +## One Package / Directory + +A directory of Go code can have **at most** one package. All `.go` files in a single directory must all belong to the same package. If they don't an error will be thrown by the compiler. This is true for main and library packages alike. diff --git a/course/12-local_development/exercises/2a-package_naming/multiple_choice.json b/course/12-local_development/exercises/2a-package_naming/multiple_choice.json new file mode 100644 index 0000000..2f9f444 --- /dev/null +++ b/course/12-local_development/exercises/2a-package_naming/multiple_choice.json @@ -0,0 +1,10 @@ +{ + "question": "Given the import path of path/to/rand, which of these is a valid package name?", + "answers": [ + "Any of these", + "path", + "rand", + "random", + "spam" + ] +} diff --git a/course/12-local_development/exercises/2a-package_naming/readme.md b/course/12-local_development/exercises/2a-package_naming/readme.md new file mode 100644 index 0000000..e8dd6e5 --- /dev/null +++ b/course/12-local_development/exercises/2a-package_naming/readme.md @@ -0,0 +1,21 @@ +# Package Naming + +## Naming Convention + +By *convention*, a package's name is the same as the last element of its import path. For instance, the `math/rand` package comprises files that begin with: + +```go +package rand +``` + +That said, package names aren't *required* to match their import path. For example, I could write a new package with the path `github.com/mailio/rand` and name the package `random`: + +```go +package random +``` + +While the above is possible, it is discouraged for the sake of consistency. + +## One Package / Directory + +A directory of Go code can have **at most** one package. All `.go` files in a single directory must all belong to the same package. If they don't an error will be thrown by the compiler. This is true for main and library packages alike. diff --git a/course/12-local_development/exercises/3-help/multiple_choice.json b/course/12-local_development/exercises/3-help/multiple_choice.json new file mode 100644 index 0000000..26d9ef8 --- /dev/null +++ b/course/12-local_development/exercises/3-help/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What does the 'go version' command print?", + "answers": [ + "go version {version} {os}/{architecture}", + "go version {os}/{architecture} {version}", + "{version} {os}/{architecture}" + ] +} diff --git a/course/12-local_development/exercises/3-help/readme.md b/course/12-local_development/exercises/3-help/readme.md new file mode 100644 index 0000000..ba03cb4 --- /dev/null +++ b/course/12-local_development/exercises/3-help/readme.md @@ -0,0 +1,32 @@ +# About this chapter + +Our courses can be completed *almost* completely in the browser, but there are some things that you need to do on your local machine for the sake of learning! + +For the majority of this chapter, coding in the browser won't be useful because you need to learn how Go code is organized in a local development environment. We'll be explaining *how* to do this and quizzing you on it. + +This section will almost entirely be quiz-style, with very little coding in the browser. As usual, if you get lost please use our discord channel as a resource: + +[Discord Community](https://boot.dev/community) + +## An example project to follow along with + +If you get lost during this chapter, refer to this `src` folder of our "Social Media Server in Go" project that can be [found here](https://github.com/bootdotdev/projects/tree/main/projects/social-media-backend-golang/10-posts_endpoints/src). It will serve as a basic example of many of the concepts in this chapter. + +## Unix + +This guide will assume you are on a Unix environment like Linux or Mac. If you're on Windows you may have to do just a *bit* of Google-ing or ask in Discord to figure out how some commands translate to Windows. + +If you are on Windows, I'd optionally recommend checking out [WSL (Windows Subsystem for Linux)](https://docs.microsoft.com/en-us/windows/wsl/install) so that you can work in a Unix environment on your local machine. + +## Download Go locally + +I typically recommend one of two ways: + +* [Official Download](https://golang.org/doc/install) +* [Webi Downloader](https://webinstall.dev/golang/) + +Make sure to use at least version `1.20`. This can be verified after installation by typing: + +```bash +go version +``` diff --git a/course/12-local_development/exercises/4-modules/multiple_choice.json b/course/12-local_development/exercises/4-modules/multiple_choice.json new file mode 100644 index 0000000..227a240 --- /dev/null +++ b/course/12-local_development/exercises/4-modules/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What is a Go module?", + "answers": [ + "A collection of packages that are released together", + "A library package", + "An executable main package", + "A file of Go code" + ] +} diff --git a/course/12-local_development/exercises/4-modules/readme.md b/course/12-local_development/exercises/4-modules/readme.md new file mode 100644 index 0000000..5946db0 --- /dev/null +++ b/course/12-local_development/exercises/4-modules/readme.md @@ -0,0 +1,33 @@ +# Modules + +Go programs are organized into *packages*. A package is a directory of Go code that's all compiled together. Functions, types, variables, and constants defined in one source file are visible to **all other source files within the same package (directory)**. + +A *repository* contains one or more *modules*. A module is a collection of Go packages that are released together. + +## A Go repository typically contains only one module, located at the root of the repository. + +A file named `go.mod` at the root of a project declares the module. It contains: + +* The module path +* The version of the Go language your project requires +* Optionally, any external package dependencies your project has + +The module path is just the import path prefix for all packages within the module. Here's an example of a `go.mod` file: + +``` +module github.com/bootdotdev/exampleproject + +go 1.20 + +require github.com/google/examplepackage v1.3.0 +``` + +Each module's path not only serves as an import path prefix for the packages within but *also indicates where the go command should look to download it*. For example, to download the module `golang.org/x/tools`, the go command would consult the repository located at [https://golang.org/x/tools](https://golang.org/x/tools). + +> An "import path" is a string used to import a package. A package's import path is its module path joined with its subdirectory within the module. For example, the module `github.com/google/go-cmp` contains a package in the directory `cmp/`. That package's import path is `github.com/google/go-cmp/cmp`. Packages in the standard library do not have a module path prefix. + +- Paraphrased from Golang.org's [code organization](https://golang.org/doc/code#Organization) + +## Do I need to put my package on GitHub? + +You don't *need* to publish your code to a remote repository before you can build it. A module can be defined locally without belonging to a repository. However, it's a good habit to keep a copy of all your projects on a remote server, like GitHub. diff --git a/course/12-local_development/exercises/4a-modules/multiple_choice.json b/course/12-local_development/exercises/4a-modules/multiple_choice.json new file mode 100644 index 0000000..e701b03 --- /dev/null +++ b/course/12-local_development/exercises/4a-modules/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Do packages in the standard library have a module path prefix?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/12-local_development/exercises/4a-modules/readme.md b/course/12-local_development/exercises/4a-modules/readme.md new file mode 100644 index 0000000..5946db0 --- /dev/null +++ b/course/12-local_development/exercises/4a-modules/readme.md @@ -0,0 +1,33 @@ +# Modules + +Go programs are organized into *packages*. A package is a directory of Go code that's all compiled together. Functions, types, variables, and constants defined in one source file are visible to **all other source files within the same package (directory)**. + +A *repository* contains one or more *modules*. A module is a collection of Go packages that are released together. + +## A Go repository typically contains only one module, located at the root of the repository. + +A file named `go.mod` at the root of a project declares the module. It contains: + +* The module path +* The version of the Go language your project requires +* Optionally, any external package dependencies your project has + +The module path is just the import path prefix for all packages within the module. Here's an example of a `go.mod` file: + +``` +module github.com/bootdotdev/exampleproject + +go 1.20 + +require github.com/google/examplepackage v1.3.0 +``` + +Each module's path not only serves as an import path prefix for the packages within but *also indicates where the go command should look to download it*. For example, to download the module `golang.org/x/tools`, the go command would consult the repository located at [https://golang.org/x/tools](https://golang.org/x/tools). + +> An "import path" is a string used to import a package. A package's import path is its module path joined with its subdirectory within the module. For example, the module `github.com/google/go-cmp` contains a package in the directory `cmp/`. That package's import path is `github.com/google/go-cmp/cmp`. Packages in the standard library do not have a module path prefix. + +- Paraphrased from Golang.org's [code organization](https://golang.org/doc/code#Organization) + +## Do I need to put my package on GitHub? + +You don't *need* to publish your code to a remote repository before you can build it. A module can be defined locally without belonging to a repository. However, it's a good habit to keep a copy of all your projects on a remote server, like GitHub. diff --git a/course/12-local_development/exercises/4b-modules/multiple_choice.json b/course/12-local_development/exercises/4b-modules/multiple_choice.json new file mode 100644 index 0000000..0e1956d --- /dev/null +++ b/course/12-local_development/exercises/4b-modules/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What is an import path?", + "answers": [ + "A module path + package subdirectory", + "An HTTP connection", + "A RESTful server" + ] +} diff --git a/course/12-local_development/exercises/4b-modules/readme.md b/course/12-local_development/exercises/4b-modules/readme.md new file mode 100644 index 0000000..5946db0 --- /dev/null +++ b/course/12-local_development/exercises/4b-modules/readme.md @@ -0,0 +1,33 @@ +# Modules + +Go programs are organized into *packages*. A package is a directory of Go code that's all compiled together. Functions, types, variables, and constants defined in one source file are visible to **all other source files within the same package (directory)**. + +A *repository* contains one or more *modules*. A module is a collection of Go packages that are released together. + +## A Go repository typically contains only one module, located at the root of the repository. + +A file named `go.mod` at the root of a project declares the module. It contains: + +* The module path +* The version of the Go language your project requires +* Optionally, any external package dependencies your project has + +The module path is just the import path prefix for all packages within the module. Here's an example of a `go.mod` file: + +``` +module github.com/bootdotdev/exampleproject + +go 1.20 + +require github.com/google/examplepackage v1.3.0 +``` + +Each module's path not only serves as an import path prefix for the packages within but *also indicates where the go command should look to download it*. For example, to download the module `golang.org/x/tools`, the go command would consult the repository located at [https://golang.org/x/tools](https://golang.org/x/tools). + +> An "import path" is a string used to import a package. A package's import path is its module path joined with its subdirectory within the module. For example, the module `github.com/google/go-cmp` contains a package in the directory `cmp/`. That package's import path is `github.com/google/go-cmp/cmp`. Packages in the standard library do not have a module path prefix. + +- Paraphrased from Golang.org's [code organization](https://golang.org/doc/code#Organization) + +## Do I need to put my package on GitHub? + +You don't *need* to publish your code to a remote repository before you can build it. A module can be defined locally without belonging to a repository. However, it's a good habit to keep a copy of all your projects on a remote server, like GitHub. diff --git a/course/12-local_development/exercises/5-gopath/multiple_choice.json b/course/12-local_development/exercises/5-gopath/multiple_choice.json new file mode 100644 index 0000000..744eb52 --- /dev/null +++ b/course/12-local_development/exercises/5-gopath/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Do you need to put your code inside of your GOPATH?", + "answers": [ + "No, in fact you shouldn't", + "It doesn't matter", + "Yes" + ] +} diff --git a/course/12-local_development/exercises/5-gopath/readme.md b/course/12-local_development/exercises/5-gopath/readme.md new file mode 100644 index 0000000..ed14e6d --- /dev/null +++ b/course/12-local_development/exercises/5-gopath/readme.md @@ -0,0 +1,23 @@ +# Setting up your machine + +Your machine will contain many version control *repositories* (managed by Git, for example). + +Each repository contains one or more *packages*, but will typically be a single *module*. + +Each package consists of one or more *Go source files* in a single directory. + +The path to a package's directory determines its *import path* and where it can be downloaded from if you decide to host it on a remote version control system like Github or Gitlab. + +## A note on GOPATH + +The $GOPATH environment variable will be set by default somewhere on your machine (typically in the home directory, `~/go`). Since we will be working in the new "Go modules" setup, you *don't need to worry about that*. If you read something online about setting up your GOPATH, that documentation is probably out of date. + +These days you should *avoid* working in the `$GOPATH/src` directory. Again, that's the old way of doing things and can cause unexpected issues, so better to just avoid it. + +## Get into your workspace + +Navigate to a location on your machine where you want to store some code. For example, I store all my code in `~/workspace`, then organize it into subfolders based on the remote location. For example, + +`~/workspace/github.com/wagslane/go-password-validator` = [https://github.com/wagslane/go-password-validator](https://github.com/wagslane/go-password-validator) + +That said, you can put your code wherever you want. diff --git a/course/12-local_development/exercises/6-first_program/multiple_choice.json b/course/12-local_development/exercises/6-first_program/multiple_choice.json new file mode 100644 index 0000000..7384b0c --- /dev/null +++ b/course/12-local_development/exercises/6-first_program/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Why does Go include a remote URL in module paths?", + "answers": [ + "To simplify remote downloading of packages", + "To confuse new gophers", + "To ensure that developers are using source control" + ] +} diff --git a/course/12-local_development/exercises/6-first_program/readme.md b/course/12-local_development/exercises/6-first_program/readme.md new file mode 100644 index 0000000..4b9150f --- /dev/null +++ b/course/12-local_development/exercises/6-first_program/readme.md @@ -0,0 +1,22 @@ +# First Local Program + +Once inside your personal workspace, create a new directory and enter it: + +```bash +mkdir hellogo +cd hellogo +``` + +Inside the directory declare your module's name: + +```bash +go mod init {REMOTE}/{USERNAME}/hellogo +``` + +Where `{REMOTE}` is your preferred remote source provider (i.e. `github.com`) and `{USERNAME}` is your Git username. If you don't use a remote provider yet, just use `example.com/username/hellogo` + +Print your `go.mod` file: + +```bash +cat go.mod +``` diff --git a/course/12-local_development/exercises/6a-first_program/multiple_choice.json b/course/12-local_development/exercises/6a-first_program/multiple_choice.json new file mode 100644 index 0000000..aae5ea9 --- /dev/null +++ b/course/12-local_development/exercises/6a-first_program/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "What is hellogo in our case?", + "answers": [ + "The repository/directory name", + "The module path prefix" + ] +} diff --git a/course/12-local_development/exercises/6a-first_program/readme.md b/course/12-local_development/exercises/6a-first_program/readme.md new file mode 100644 index 0000000..4b9150f --- /dev/null +++ b/course/12-local_development/exercises/6a-first_program/readme.md @@ -0,0 +1,22 @@ +# First Local Program + +Once inside your personal workspace, create a new directory and enter it: + +```bash +mkdir hellogo +cd hellogo +``` + +Inside the directory declare your module's name: + +```bash +go mod init {REMOTE}/{USERNAME}/hellogo +``` + +Where `{REMOTE}` is your preferred remote source provider (i.e. `github.com`) and `{USERNAME}` is your Git username. If you don't use a remote provider yet, just use `example.com/username/hellogo` + +Print your `go.mod` file: + +```bash +cat go.mod +``` diff --git a/course/12-local_development/exercises/6b-first_program/multiple_choice.json b/course/12-local_development/exercises/6b-first_program/multiple_choice.json new file mode 100644 index 0000000..2c634e9 --- /dev/null +++ b/course/12-local_development/exercises/6b-first_program/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What does the first line of go.mod contain?", + "answers": [ + "module {REMOTE}/{USERNAME}/hellogo", + "{REMOTE}/{USERNAME}/hellogo", + "module hellogo" + ] +} diff --git a/course/12-local_development/exercises/6b-first_program/readme.md b/course/12-local_development/exercises/6b-first_program/readme.md new file mode 100644 index 0000000..4b9150f --- /dev/null +++ b/course/12-local_development/exercises/6b-first_program/readme.md @@ -0,0 +1,22 @@ +# First Local Program + +Once inside your personal workspace, create a new directory and enter it: + +```bash +mkdir hellogo +cd hellogo +``` + +Inside the directory declare your module's name: + +```bash +go mod init {REMOTE}/{USERNAME}/hellogo +``` + +Where `{REMOTE}` is your preferred remote source provider (i.e. `github.com`) and `{USERNAME}` is your Git username. If you don't use a remote provider yet, just use `example.com/username/hellogo` + +Print your `go.mod` file: + +```bash +cat go.mod +``` diff --git a/course/12-local_development/exercises/7-go_run/multiple_choice.json b/course/12-local_development/exercises/7-go_run/multiple_choice.json new file mode 100644 index 0000000..30368f0 --- /dev/null +++ b/course/12-local_development/exercises/7-go_run/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Does 'go run' build a production executable?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/12-local_development/exercises/7-go_run/readme.md b/course/12-local_development/exercises/7-go_run/readme.md new file mode 100644 index 0000000..1a1d741 --- /dev/null +++ b/course/12-local_development/exercises/7-go_run/readme.md @@ -0,0 +1,31 @@ +# Go Run + +Inside `hellogo`, create a new file called `main.go`. + +Conventionally, the file in the `main` package that contains the `main()` function is called `main.go`. + +Paste the following code into your file: + +```go +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} +``` + +## Run the code + +```bash +go run main.go +``` + +The `go run` command is used to quickly compile and run a Go package. The compiled binary is *not* saved in your working directory. Use `go build` instead to compile production executables. + +I rarely use `go run` other than to quickly do some testing or debugging. + +## Further reading + +Execute `go help run` in your shell and read the instructions. diff --git a/course/12-local_development/exercises/7a-go_run/multiple_choice.json b/course/12-local_development/exercises/7a-go_run/multiple_choice.json new file mode 100644 index 0000000..d3b8a0b --- /dev/null +++ b/course/12-local_development/exercises/7a-go_run/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Which can 'go run' accept as arguments?", + "answers": [ + "Both", + "File names", + "Package names" + ] +} diff --git a/course/12-local_development/exercises/7a-go_run/readme.md b/course/12-local_development/exercises/7a-go_run/readme.md new file mode 100644 index 0000000..1a1d741 --- /dev/null +++ b/course/12-local_development/exercises/7a-go_run/readme.md @@ -0,0 +1,31 @@ +# Go Run + +Inside `hellogo`, create a new file called `main.go`. + +Conventionally, the file in the `main` package that contains the `main()` function is called `main.go`. + +Paste the following code into your file: + +```go +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} +``` + +## Run the code + +```bash +go run main.go +``` + +The `go run` command is used to quickly compile and run a Go package. The compiled binary is *not* saved in your working directory. Use `go build` instead to compile production executables. + +I rarely use `go run` other than to quickly do some testing or debugging. + +## Further reading + +Execute `go help run` in your shell and read the instructions. diff --git a/course/12-local_development/exercises/8-go_build/multiple_choice.json b/course/12-local_development/exercises/8-go_build/multiple_choice.json new file mode 100644 index 0000000..ce7eeee --- /dev/null +++ b/course/12-local_development/exercises/8-go_build/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What was created after running 'go build'?", + "answers": [ + "An executable file named 'hellogo'", + "An executable file named main", + "A package named cmd" + ] +} diff --git a/course/12-local_development/exercises/8-go_build/readme.md b/course/12-local_development/exercises/8-go_build/readme.md new file mode 100644 index 0000000..de804e8 --- /dev/null +++ b/course/12-local_development/exercises/8-go_build/readme.md @@ -0,0 +1,17 @@ +# Go Build + +`go build` compiles go code into an executable program + +## Build an executable + +Ensure you are in your hellogo repo, then run: + +```bash +go build +``` + +Run the new program: + +```bash +./hellogo +``` diff --git a/course/12-local_development/exercises/8a-go_build/multiple_choice.json b/course/12-local_development/exercises/8a-go_build/multiple_choice.json new file mode 100644 index 0000000..7164a89 --- /dev/null +++ b/course/12-local_development/exercises/8a-go_build/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What happens when you run './hellogo'?", + "answers": [ + "'hello world' is printed", + "The program panics", + "The code compiles" + ] +} diff --git a/course/12-local_development/exercises/8a-go_build/readme.md b/course/12-local_development/exercises/8a-go_build/readme.md new file mode 100644 index 0000000..de804e8 --- /dev/null +++ b/course/12-local_development/exercises/8a-go_build/readme.md @@ -0,0 +1,17 @@ +# Go Build + +`go build` compiles go code into an executable program + +## Build an executable + +Ensure you are in your hellogo repo, then run: + +```bash +go build +``` + +Run the new program: + +```bash +./hellogo +``` diff --git a/course/13-channels/challenges/1-channels_practice/code.go b/course/13-channels/challenges/1-channels_practice/code.go new file mode 100644 index 0000000..7a3dcd6 --- /dev/null +++ b/course/13-channels/challenges/1-channels_practice/code.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "time" +) + +func pingPong(numPings int) { + pings := make(chan struct{}) + pongs := make(chan struct{}) + go ponger(pings, pongs) + go pinger(pings, pongs, numPings) +} + +// TEST SUITE - Don't touch below this line + +func pinger(pings, pongs chan struct{}, numPings int) { + go func() { + sleepTime := 50 * time.Millisecond + for i := 0; i < numPings; i++ { + fmt.Println("ping", i, "sent") + pings <- struct{}{} + time.Sleep(sleepTime) + sleepTime *= 2 + } + close(pings) + }() + i := 0 + for range pongs { + fmt.Println("pong", i, "got") + i++ + } + fmt.Println("pongs done") +} + +func ponger(pings, pongs chan struct{}) { + i := 0 + for range pings { + fmt.Println("ping", i, "got", "pong", i, "sent") + pongs <- struct{}{} + i++ + } + fmt.Println("pings done") + close(pongs) +} + +func test(numPings int) { + fmt.Println("Starting game...") + pingPong(numPings) + fmt.Println("===== Game over =====") +} + +func main() { + test(4) + test(3) + test(2) +} diff --git a/course/13-channels/challenges/1-channels_practice/complete.go b/course/13-channels/challenges/1-channels_practice/complete.go new file mode 100644 index 0000000..f0a839b --- /dev/null +++ b/course/13-channels/challenges/1-channels_practice/complete.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "time" +) + +func pingPong(numPings int) { + pings := make(chan struct{}) + pongs := make(chan struct{}) + go ponger(pings, pongs) + pinger(pings, pongs, numPings) +} + +// TEST SUITE - Don't touch below this line + +func pinger(pings, pongs chan struct{}, numPings int) { + go func() { + sleepTime := 50 * time.Millisecond + for i := 0; i < numPings; i++ { + fmt.Println("ping", i, "sent") + pings <- struct{}{} + time.Sleep(sleepTime) + sleepTime *= 2 + } + close(pings) + }() + i := 0 + for range pongs { + fmt.Println("pong", i, "got") + i++ + } + fmt.Println("pongs done") +} + +func ponger(pings, pongs chan struct{}) { + i := 0 + for range pings { + fmt.Println("ping", i, "got", "pong", i, "sent") + pongs <- struct{}{} + i++ + } + fmt.Println("pings done") + close(pongs) +} + +func test(numPings int) { + fmt.Println("Starting game...") + pingPong(numPings) + fmt.Println("===== Game over =====") +} + +func main() { + test(4) + test(3) + test(2) +} diff --git a/course/13-channels/challenges/1-channels_practice/expected.txt b/course/13-channels/challenges/1-channels_practice/expected.txt new file mode 100644 index 0000000..1b24e4f --- /dev/null +++ b/course/13-channels/challenges/1-channels_practice/expected.txt @@ -0,0 +1,39 @@ +Starting game... +ping 0 sent +ping 0 got pong 0 sent +pong 0 got +ping 1 sent +ping 1 got pong 1 sent +pong 1 got +ping 2 sent +ping 2 got pong 2 sent +pong 2 got +ping 3 sent +ping 3 got pong 3 sent +pong 3 got +pings done +pongs done +===== Game over ===== +Starting game... +ping 0 sent +ping 0 got pong 0 sent +pong 0 got +ping 1 sent +ping 1 got pong 1 sent +pong 1 got +ping 2 sent +ping 2 got pong 2 sent +pong 2 got +pings done +pongs done +===== Game over ===== +Starting game... +ping 0 sent +ping 0 got pong 0 sent +pong 0 got +ping 1 sent +ping 1 got pong 1 sent +pong 1 got +pings done +pongs done +===== Game over ===== diff --git a/course/13-channels/challenges/1-channels_practice/readme.md b/course/13-channels/challenges/1-channels_practice/readme.md new file mode 100644 index 0000000..811f421 --- /dev/null +++ b/course/13-channels/challenges/1-channels_practice/readme.md @@ -0,0 +1,7 @@ +# Ping Pong + +Many tech companies play ping pong in the office during break time. At Mailio, we play virtual ping pong using channels. + +## Assignment + +Fix the bug in the `pingPong` function. It shouldn't `return` until the entire game of ping pong is complete. diff --git a/course/13-channels/exercises/1-intro/code.go b/course/13-channels/exercises/1-intro/code.go new file mode 100644 index 0000000..231c42d --- /dev/null +++ b/course/13-channels/exercises/1-intro/code.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "time" +) + +func sendEmail(message string) { + func() { + time.Sleep(time.Millisecond * 250) + fmt.Printf("Email received: '%s'\n", message) + }() + fmt.Printf("Email sent: '%s'\n", message) +} + +// Don't touch below this line + +func test(message string) { + sendEmail(message) + time.Sleep(time.Millisecond * 500) + fmt.Println("========================") +} + +func main() { + test("Hello there Stacy!") + test("Hi there John!") + test("Hey there Jane!") +} diff --git a/course/13-channels/exercises/1-intro/complete.go b/course/13-channels/exercises/1-intro/complete.go new file mode 100644 index 0000000..dae84d9 --- /dev/null +++ b/course/13-channels/exercises/1-intro/complete.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "time" +) + +func sendEmail(message string) { + go func() { + time.Sleep(time.Millisecond * 250) + fmt.Printf("Email received: '%s'\n", message) + }() + fmt.Printf("Email sent: '%s'\n", message) +} + +// Don't touch below this line + +func test(message string) { + sendEmail(message) + time.Sleep(time.Millisecond * 500) + fmt.Println("========================") +} + +func main() { + test("Hello there Stacy!") + test("Hi there John!") + test("Hey there Jane!") +} diff --git a/course/13-channels/exercises/1-intro/expected.txt b/course/13-channels/exercises/1-intro/expected.txt new file mode 100644 index 0000000..fe9d639 --- /dev/null +++ b/course/13-channels/exercises/1-intro/expected.txt @@ -0,0 +1,9 @@ +Email sent: 'Hello there Stacy!' +Email received: 'Hello there Stacy!' +======================== +Email sent: 'Hi there John!' +Email received: 'Hi there John!' +======================== +Email sent: 'Hey there Jane!' +Email received: 'Hey there Jane!' +======================== diff --git a/course/13-channels/exercises/1-intro/readme.md b/course/13-channels/exercises/1-intro/readme.md new file mode 100644 index 0000000..946a658 --- /dev/null +++ b/course/13-channels/exercises/1-intro/readme.md @@ -0,0 +1,29 @@ +# Concurrency + +## What is concurrency? + +Concurrency is the ability to perform multiple tasks at the same time. Typically, our code is executed one line at a time, one after the other. This is called *sequential execution* or *synchronous execution*. + +![concurrency](https://i.imgur.com/1pQKFgw.png) + +If the computer we're running our code on has multiple cores, we can even execute multiple tasks at *exactly* the same time. If we're running on a single core, a single code executes code at *almost* the same time by switching between tasks very quickly. Either way, the code we write looks the same in Go and takes advantage of whatever resources are available. + +## How does concurrency work in Go? + +Go was designed to be concurrent, which is a trait *fairly* unique to Go. It excels at performing many tasks simultaneously safely using a simple syntax. + +There isn't a popular programming language in existence where spawning concurrent execution is quite as elegant, at least in my opinion. + +Concurrency is as simple as using the `go` keyword when calling a function: + +```go +go doSomething() +``` + +In the example above, `doSomething()` will be executed concurrently with the rest of the code in the function. The `go` keyword is used to spawn a new *[goroutine](https://gobyexample.com/goroutines)*. + +## Assignment + +At Mailio we send *a lot* of network requests. Each email we send must go out over the internet. To serve our millions of customers, we need a single Go program to be capable of sending *thousands* of emails at once. + +Edit the `sendEmail()` function to execute its anonymous function concurrently so that the "received" message prints *after* the "sent" message. diff --git a/course/13-channels/exercises/2-channels_deadlock/code.go b/course/13-channels/exercises/2-channels_deadlock/code.go new file mode 100644 index 0000000..38cab3c --- /dev/null +++ b/course/13-channels/exercises/2-channels_deadlock/code.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "time" +) + +func filterOldEmails(emails []email) { + isOldChan := make(chan bool) + + for _, e := range emails { + if e.date.Before(time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC)) { + isOldChan <- true + continue + } + isOldChan <- false + } + + isOld := <-isOldChan + fmt.Println("email 1 is old:", isOld) + isOld = <-isOldChan + fmt.Println("email 2 is old:", isOld) + isOld = <-isOldChan + fmt.Println("email 3 is old:", isOld) +} + +// TEST SUITE -- Don't touch below this line + +type email struct { + body string + date time.Time +} + +func test(emails []email) { + filterOldEmails(emails) + fmt.Println("==========================================") +} + +func main() { + test([]email{ + { + body: "Are you going to make it?", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "I need a break", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "What were you thinking?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Yo are you okay?", + date: time.Date(2018, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Have you heard of that website Boot.dev?", + date: time.Date(2017, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "It's awesome honestly.", + date: time.Date(2016, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Today is the day!", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "What do you want for lunch?", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Why are you the way that you are?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Did we do it?", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Letsa Go!", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Okay...?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) +} diff --git a/course/13-channels/exercises/2-channels_deadlock/complete.go b/course/13-channels/exercises/2-channels_deadlock/complete.go new file mode 100644 index 0000000..870224d --- /dev/null +++ b/course/13-channels/exercises/2-channels_deadlock/complete.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "time" +) + +func filterOldEmails(emails []email) { + isOldChan := make(chan bool) + + go func() { + for _, e := range emails { + if e.date.Before(time.Date(2020, 0, 0, 0, 0, 0, 0, time.UTC)) { + isOldChan <- true + continue + } + isOldChan <- false + } + }() + + isOld := <-isOldChan + fmt.Println("email 1 is old:", isOld) + isOld = <-isOldChan + fmt.Println("email 2 is old:", isOld) + isOld = <-isOldChan + fmt.Println("email 3 is old:", isOld) +} + +// TEST SUITE -- Don't touch below this line + +type email struct { + body string + date time.Time +} + +func test(emails []email) { + filterOldEmails(emails) + fmt.Println("==========================================") +} + +func main() { + test([]email{ + { + body: "Are you going to make it?", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "I need a break", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "What were you thinking?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Yo are you okay?", + date: time.Date(2018, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Have you heard of that website Boot.dev?", + date: time.Date(2017, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "It's awesome honestly.", + date: time.Date(2016, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Today is the day!", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "What do you want for lunch?", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Why are you the way that you are?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) + test([]email{ + { + body: "Did we do it?", + date: time.Date(2019, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Letsa Go!", + date: time.Date(2021, 0, 0, 0, 0, 0, 0, time.UTC), + }, + { + body: "Okay...?", + date: time.Date(2022, 0, 0, 0, 0, 0, 0, time.UTC), + }, + }) +} diff --git a/course/13-channels/exercises/2-channels_deadlock/expected.txt b/course/13-channels/exercises/2-channels_deadlock/expected.txt new file mode 100644 index 0000000..b05cade --- /dev/null +++ b/course/13-channels/exercises/2-channels_deadlock/expected.txt @@ -0,0 +1,16 @@ +email 1 is old: true +email 2 is old: false +email 3 is old: false +========================================== +email 1 is old: true +email 2 is old: true +email 3 is old: true +========================================== +email 1 is old: true +email 2 is old: false +email 3 is old: false +========================================== +email 1 is old: true +email 2 is old: false +email 3 is old: false +========================================== diff --git a/course/13-channels/exercises/2-channels_deadlock/readme.md b/course/13-channels/exercises/2-channels_deadlock/readme.md new file mode 100644 index 0000000..21912b1 --- /dev/null +++ b/course/13-channels/exercises/2-channels_deadlock/readme.md @@ -0,0 +1,37 @@ +# Channels + +Channels are a typed, thread-safe queue. Channels allow different goroutines to communicate with each other. + +## Create a channel + +Like maps and slices, channels must be created *before* use. They also use the same `make` keyword: + +```go +ch := make(chan int) +``` + +## Send data to a channel + +```go +ch <- 69 +``` + +The `<-` operator is called the *channel operator*. Data flows in the direction of the arrow. This operation will *block* until another goroutine is ready to receive the value. + +## Receive data from a channel + +```go +v := <-ch +``` + +This reads and removes a value from the channel and saves it into the variable `v`. This operation will *block* until there is a value in the channel to be read. + +## Blocking and deadlocks + +A [deadlock](https://yourbasic.org/golang/detect-deadlock/#:~:text=yourbasic.org%2Fgolang,look%20at%20this%20simple%20example.) is when a group of goroutines are all blocking so none of them can continue. This is a common bug that you need to watch out for in concurrent programming. + +## Assignment + +Run the program. You'll see that it deadlocks and never exits. The `filterOldEmails` function is trying to send on a channel and there is no other goroutine running that can accept the value *from* the channel. + +Fix the deadlock by spawning an anonymous goroutine to write to the `isOldChan` channel instead of using the same goroutine that's reading from it. diff --git a/course/13-channels/exercises/3-channels_send/code.go b/course/13-channels/exercises/3-channels_send/code.go new file mode 100644 index 0000000..fe179a5 --- /dev/null +++ b/course/13-channels/exercises/3-channels_send/code.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "time" +) + +func waitForDbs(numDBs int, dbChan chan struct{}) { + // ? +} + +// don't touch below this line + +func test(numDBs int) { + dbChan := getDatabasesChannel(numDBs) + fmt.Printf("Waiting for %v databases...\n", numDBs) + waitForDbs(numDBs, dbChan) + time.Sleep(time.Millisecond * 10) // ensure the last print statement happens + fmt.Println("All databases are online!") + fmt.Println("=====================================") +} + +func main() { + test(3) + test(4) + test(5) +} + +func getDatabasesChannel(numDBs int) chan struct{} { + ch := make(chan struct{}) + go func() { + for i := 0; i < numDBs; i++ { + ch <- struct{}{} + fmt.Printf("Database %v is online\n", i+1) + } + }() + return ch +} diff --git a/course/13-channels/exercises/3-channels_send/complete.go b/course/13-channels/exercises/3-channels_send/complete.go new file mode 100644 index 0000000..b6f56ab --- /dev/null +++ b/course/13-channels/exercises/3-channels_send/complete.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "time" +) + +func waitForDbs(numDBs int, dbChan chan struct{}) { + for i := 0; i < numDBs; i++ { + <-dbChan + } +} + +// don't touch below this line + +func test(numDBs int) { + dbChan := getDatabasesChannel(numDBs) + fmt.Printf("Waiting for %v databases...\n", numDBs) + waitForDbs(numDBs, dbChan) + time.Sleep(time.Millisecond * 10) // ensure the last print statement happens + fmt.Println("All databases are online!") + fmt.Println("=====================================") +} + +func main() { + test(3) + test(4) + test(5) +} + +func getDatabasesChannel(numDBs int) chan struct{} { + ch := make(chan struct{}) + go func() { + for i := 0; i < numDBs; i++ { + ch <- struct{}{} + fmt.Printf("Database %v is online\n", i+1) + } + }() + return ch +} diff --git a/course/13-channels/exercises/3-channels_send/expected.txt b/course/13-channels/exercises/3-channels_send/expected.txt new file mode 100644 index 0000000..3673a99 --- /dev/null +++ b/course/13-channels/exercises/3-channels_send/expected.txt @@ -0,0 +1,21 @@ +Waiting for 3 databases... +Database 1 is online +Database 2 is online +Database 3 is online +All databases are online! +===================================== +Waiting for 4 databases... +Database 1 is online +Database 2 is online +Database 3 is online +Database 4 is online +All databases are online! +===================================== +Waiting for 5 databases... +Database 1 is online +Database 2 is online +Database 3 is online +Database 4 is online +Database 5 is online +All databases are online! +===================================== diff --git a/course/13-channels/exercises/3-channels_send/readme.md b/course/13-channels/exercises/3-channels_send/readme.md new file mode 100644 index 0000000..51992d3 --- /dev/null +++ b/course/13-channels/exercises/3-channels_send/readme.md @@ -0,0 +1,17 @@ +# Channels + +Empty structs are often used as `tokens` in Go programs. In this context, a token is a [unary](https://en.wikipedia.org/wiki/Unary_operation) value. In other words, we don't care *what* is passed through the channel. We care *when* and *if* it is passed. + +We can block and wait until *something* is sent on a channel using the following syntax + +```go +<-ch +``` + +This will block until it pops a single item off the channel, then continue, discarding the item. + +## Assignment + +Our Mailio server isn't able to boot up until it receives the signal that its databases are all online, and it learns about them being online by waiting for tokens (empty structs) on a channel. + +Complete the `waitForDbs` function. It should block until it receives `numDBs` tokens on the `dbChan` channel. Each time it reads a token the `getDatabasesChannel` goroutine will print a message to the console for you. diff --git a/course/13-channels/exercises/4-buffered_channels/code.go b/course/13-channels/exercises/4-buffered_channels/code.go new file mode 100644 index 0000000..57c56fb --- /dev/null +++ b/course/13-channels/exercises/4-buffered_channels/code.go @@ -0,0 +1,34 @@ +package main + +import "fmt" + +func addEmailsToQueue(emails []string) chan string { + emailsToSend := make(chan string) + for _, email := range emails { + emailsToSend <- email + } + return emailsToSend +} + +// TEST SUITE - Don't Touch Below This Line + +func sendEmails(batchSize int, ch chan string) { + for i := 0; i < batchSize; i++ { + email := <-ch + fmt.Println("Sending email:", email) + } +} + +func test(emails ...string) { + fmt.Printf("Adding %v emails to queue...\n", len(emails)) + ch := addEmailsToQueue(emails) + fmt.Println("Sending emails...") + sendEmails(len(emails), ch) + fmt.Println("==========================================") +} + +func main() { + test("Hello John, tell Kathy I said hi", "Whazzup bruther") + test("I find that hard to believe.", "When? I don't know if I can", "What time are you thinking?") + test("She says hi!", "Yeah its tomorrow. So we're good.", "Cool see you then!", "Bye!") +} diff --git a/course/13-channels/exercises/4-buffered_channels/complete.go b/course/13-channels/exercises/4-buffered_channels/complete.go new file mode 100644 index 0000000..d6f8f4d --- /dev/null +++ b/course/13-channels/exercises/4-buffered_channels/complete.go @@ -0,0 +1,34 @@ +package main + +import "fmt" + +func addEmailsToQueue(emails []string) chan string { + emailsToSend := make(chan string, len(emails)) + for _, email := range emails { + emailsToSend <- email + } + return emailsToSend +} + +// TEST SUITE - Don't Touch Below This Line + +func sendEmails(batchSize int, ch chan string) { + for i := 0; i < batchSize; i++ { + email := <-ch + fmt.Println("Sending email:", email) + } +} + +func test(emails ...string) { + fmt.Printf("Adding %v emails to queue...\n", len(emails)) + ch := addEmailsToQueue(emails) + fmt.Println("Sending emails...") + sendEmails(len(emails), ch) + fmt.Println("==========================================") +} + +func main() { + test("Hello John, tell Kathy I said hi", "Whazzup bruther") + test("I find that hard to believe.", "When? I don't know if I can", "What time are you thinking?") + test("She says hi!", "Yeah its tomorrow. So we're good.", "Cool see you then!", "Bye!") +} diff --git a/course/13-channels/exercises/4-buffered_channels/expected.txt b/course/13-channels/exercises/4-buffered_channels/expected.txt new file mode 100644 index 0000000..a05d687 --- /dev/null +++ b/course/13-channels/exercises/4-buffered_channels/expected.txt @@ -0,0 +1,18 @@ +Adding 2 emails to queue... +Sending emails... +Sending email: Hello John, tell Kathy I said hi +Sending email: Whazzup bruther +========================================== +Adding 3 emails to queue... +Sending emails... +Sending email: I find that hard to believe. +Sending email: When? I don't know if I can +Sending email: What time are you thinking? +========================================== +Adding 4 emails to queue... +Sending emails... +Sending email: She says hi! +Sending email: Yeah its tomorrow. So we're good. +Sending email: Cool see you then! +Sending email: Bye! +========================================== diff --git a/course/13-channels/exercises/4-buffered_channels/readme.md b/course/13-channels/exercises/4-buffered_channels/readme.md new file mode 100644 index 0000000..aacb31a --- /dev/null +++ b/course/13-channels/exercises/4-buffered_channels/readme.md @@ -0,0 +1,21 @@ +# Buffered Channels + +Channels can *optionally* be buffered. + +## Creating a channel with a buffer + +You can provide a buffer length as the second argument to `make()` to create a buffered channel: + +```go +ch := make(chan int, 100) +``` + +Sending on a buffered channel only blocks when the buffer is *full*. + +Receiving blocks only when the buffer is *empty*. + +## Assignment + +We want to be able to send emails in *batches*. A *writing* goroutine will write an entire batch of email messages to a buffered channel, and later, once the channel is full, a *reading* goroutine will read all of the messages from the channel and send them out to our clients. + +Complete the `addEmailsToQueue` function. It should create a buffered channel with a buffer large enough to store all of the `emails` it's given. It should then write the emails to the channel in order, and finally return the channel. diff --git a/course/13-channels/exercises/5-close/code.go b/course/13-channels/exercises/5-close/code.go new file mode 100644 index 0000000..7801c42 --- /dev/null +++ b/course/13-channels/exercises/5-close/code.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "time" +) + +func countReports(numSentCh chan int) int { + // ? +} + +// TEST SUITE - Don't touch below this line + +func test(numBatches int) { + numSentCh := make(chan int) + go sendReports(numBatches, numSentCh) + + fmt.Println("Start counting...") + numReports := countReports(numSentCh) + fmt.Printf("%v reports sent!\n", numReports) + fmt.Println("========================") +} + +func main() { + test(3) + test(4) + test(5) + test(6) +} + +func sendReports(numBatches int, ch chan int) { + for i := 0; i < numBatches; i++ { + numReports := i*23 + 32%17 + ch <- numReports + fmt.Printf("Sent batch of %v reports\n", numReports) + time.Sleep(time.Millisecond * 100) + } + close(ch) +} diff --git a/course/13-channels/exercises/5-close/complete.go b/course/13-channels/exercises/5-close/complete.go new file mode 100644 index 0000000..a5c888b --- /dev/null +++ b/course/13-channels/exercises/5-close/complete.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "time" +) + +func countReports(numSentCh chan int) int { + total := 0 + for { + numSent, ok := <-numSentCh + if !ok { + break + } + total += numSent + } + return total +} + +// TEST SUITE - Don't touch below this line + +func test(numBatches int) { + numSentCh := make(chan int) + go sendReports(numBatches, numSentCh) + + fmt.Println("Start counting...") + numReports := countReports(numSentCh) + fmt.Printf("%v reports sent!\n", numReports) + fmt.Println("========================") +} + +func main() { + test(3) + test(4) + test(5) + test(6) +} + +func sendReports(numBatches int, ch chan int) { + for i := 0; i < numBatches; i++ { + numReports := i*23 + 32%17 + ch <- numReports + fmt.Printf("Sent batch of %v reports\n", numReports) + time.Sleep(time.Millisecond * 100) + } + close(ch) +} diff --git a/course/13-channels/exercises/5-close/expected.txt b/course/13-channels/exercises/5-close/expected.txt new file mode 100644 index 0000000..cd04f4d --- /dev/null +++ b/course/13-channels/exercises/5-close/expected.txt @@ -0,0 +1,30 @@ +Start counting... +Sent batch of 15 reports +Sent batch of 38 reports +Sent batch of 61 reports +114 reports sent! +======================== +Start counting... +Sent batch of 15 reports +Sent batch of 38 reports +Sent batch of 61 reports +Sent batch of 84 reports +198 reports sent! +======================== +Start counting... +Sent batch of 15 reports +Sent batch of 38 reports +Sent batch of 61 reports +Sent batch of 84 reports +Sent batch of 107 reports +305 reports sent! +======================== +Start counting... +Sent batch of 15 reports +Sent batch of 38 reports +Sent batch of 61 reports +Sent batch of 84 reports +Sent batch of 107 reports +Sent batch of 130 reports +435 reports sent! +======================== diff --git a/course/13-channels/exercises/5-close/readme.md b/course/13-channels/exercises/5-close/readme.md new file mode 100644 index 0000000..9e21481 --- /dev/null +++ b/course/13-channels/exercises/5-close/readme.md @@ -0,0 +1,40 @@ +# Closing channels in Go + +Channels can be explicitly closed by a *sender*: + +```go +ch := make(chan int) + +// do some stuff with the channel + +close(ch) +``` + +## Checking if a channel is closed + +Similar to the `ok` value when accessing data in a `map`, receivers can check the `ok` value when receiving from a channel to test if a channel was closed. + +```go +v, ok := <-ch +``` + +ok is `false` if the channel is empty and closed. + +## Don't send on a closed channel + +Sending on a closed channel will cause a panic. A panic on the main goroutine will cause the entire program to crash, and a panic in any other goroutine will cause *that goroutine* to crash. + +Closing isn't necessary. There's nothing wrong with leaving channels open, they'll still be garbage collected if they're unused. You should close channels to indicate explicitly to a receiver that nothing else is going to come across. + +## Assignment + +At Mailio we're all about keeping track of what our systems are up to with great logging and [telemetry](https://en.wikipedia.org/wiki/Telemetry). + +The `sendReports` function sends out a batch of reports to our clients and reports back how many were sent across a channel. It closes the channel when it's done. + +Complete the `countReports` function. It should: + +* Use an infinite `for` loop to read from the channel: +* If the channel is closed, break out of the loop +* Otherwise, keep a running total of the number of reports sent +* Return the total number of reports sent diff --git a/course/13-channels/exercises/6-range/code.go b/course/13-channels/exercises/6-range/code.go new file mode 100644 index 0000000..9875e42 --- /dev/null +++ b/course/13-channels/exercises/6-range/code.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "time" +) + +func concurrrentFib(n int) { + // ? +} + +// TEST SUITE - Don't touch below this line + +func test(n int) { + fmt.Printf("Printing %v numbers...\n", n) + concurrrentFib(n) + fmt.Println("==============================") +} + +func main() { + test(10) + test(5) + test(20) + test(13) +} + +func fibonacci(n int, ch chan int) { + x, y := 0, 1 + for i := 0; i < n; i++ { + ch <- x + x, y = y, x+y + time.Sleep(time.Millisecond * 10) + } + close(ch) +} diff --git a/course/13-channels/exercises/6-range/complete.go b/course/13-channels/exercises/6-range/complete.go new file mode 100644 index 0000000..2afc467 --- /dev/null +++ b/course/13-channels/exercises/6-range/complete.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "time" +) + +func concurrrentFib(n int) { + ch := make(chan int) + go fibonacci(n, ch) + + for v := range ch { + fmt.Println(v) + } +} + +// TEST SUITE - Don't touch below this line + +func test(n int) { + fmt.Printf("Printing %v numbers...\n", n) + concurrrentFib(n) + fmt.Println("==============================") +} + +func main() { + test(10) + test(5) + test(20) + test(13) +} + +func fibonacci(n int, ch chan int) { + x, y := 0, 1 + for i := 0; i < n; i++ { + ch <- x + x, y = y, x+y + time.Sleep(time.Millisecond * 10) + } + close(ch) +} diff --git a/course/13-channels/exercises/6-range/expected.txt b/course/13-channels/exercises/6-range/expected.txt new file mode 100644 index 0000000..30e42ae --- /dev/null +++ b/course/13-channels/exercises/6-range/expected.txt @@ -0,0 +1,56 @@ +Printing 10 numbers... +0 +1 +1 +2 +3 +5 +8 +13 +21 +34 +============================== +Printing 5 numbers... +0 +1 +1 +2 +3 +============================== +Printing 20 numbers... +0 +1 +1 +2 +3 +5 +8 +13 +21 +34 +55 +89 +144 +233 +377 +610 +987 +1597 +2584 +4181 +============================== +Printing 13 numbers... +0 +1 +1 +2 +3 +5 +8 +13 +21 +34 +55 +89 +144 +============================== diff --git a/course/13-channels/exercises/6-range/readme.md b/course/13-channels/exercises/6-range/readme.md new file mode 100644 index 0000000..dbc2d7b --- /dev/null +++ b/course/13-channels/exercises/6-range/readme.md @@ -0,0 +1,21 @@ +# Range + +Similar to slices and maps, channels can be ranged over. + +```go +for item := range ch { + // item is the next value received from the channel +} +``` + +This example will receive values over the channel (blocking at each iteration if nothing new is there) and will exit only when the channel is closed. + +## Assignment + +It's that time again, Mailio is hiring and we've been assigned to do the interview. For some reason, the [Fibonacci sequence](https://en.wikipedia.org/wiki/Fibonacci_number) is Mailio's interview problem of choice. We've been tasked with building a small toy program we can use in the interview. + +Complete the `concurrrentFib` function. It should: + +* Create a new channel of `int`s +* Call `fibonacci` in a goroutine, passing it the channel and the number of Fibonacci numbers to generate, `n` +* Use a `range` loop to read from the channel and print out the numbers one by one, each on a new line diff --git a/course/13-channels/exercises/7-select/code.go b/course/13-channels/exercises/7-select/code.go new file mode 100644 index 0000000..08bee7e --- /dev/null +++ b/course/13-channels/exercises/7-select/code.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "math/rand" + "time" +) + +func logMessages(chEmails, chSms chan string) { + // ? +} + +// TEST SUITE - Don't touch below this line + +func logSms(sms string) { + fmt.Println("SMS:", sms) +} + +func logEmail(email string) { + fmt.Println("Email:", email) +} + +func test(sms []string, emails []string) { + fmt.Println("Starting...") + + chSms, chEmails := sendToLogger(sms, emails) + + logMessages(chEmails, chSms) + fmt.Println("===============================") +} + +func main() { + rand.Seed(0) + test( + []string{ + "hi friend", + "What's going on?", + "Welcome to the business", + "I'll pay you to be my friend", + }, + []string{ + "Will you make your appointment?", + "Let's be friends", + "What are you doing?", + "I can't believe you've done this.", + }, + ) + test( + []string{ + "this song slaps hard", + "yooo hoooo", + "i'm a big fan", + }, + []string{ + "What do you think of this song?", + "I hate this band", + "Can you believe this song?", + }, + ) +} + +func sendToLogger(sms, emails []string) (chSms, chEmails chan string) { + chSms = make(chan string) + chEmails = make(chan string) + go func() { + for i := 0; i < len(sms) && i < len(emails); i++ { + done := make(chan struct{}) + s := sms[i] + e := emails[i] + t1 := time.Millisecond * time.Duration(rand.Intn(1000)) + t2 := time.Millisecond * time.Duration(rand.Intn(1000)) + go func() { + time.Sleep(t1) + chSms <- s + done <- struct{}{} + }() + go func() { + time.Sleep(t2) + chEmails <- e + done <- struct{}{} + }() + <-done + <-done + time.Sleep(10 * time.Millisecond) + } + close(chSms) + close(chEmails) + }() + return chSms, chEmails +} diff --git a/course/13-channels/exercises/7-select/complete.go b/course/13-channels/exercises/7-select/complete.go new file mode 100644 index 0000000..43036a7 --- /dev/null +++ b/course/13-channels/exercises/7-select/complete.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "math/rand" + "time" +) + +func logMessages(chEmails, chSms chan string) { + for { + select { + case email, ok := <-chEmails: + if !ok { + return + } + logEmail(email) + case sms, ok := <-chSms: + if !ok { + return + } + logSms(sms) + } + } +} + +// TEST SUITE - Don't touch below this line + +func logSms(sms string) { + fmt.Println("SMS:", sms) +} + +func logEmail(email string) { + fmt.Println("Email:", email) +} + +func test(sms []string, emails []string) { + fmt.Println("Starting...") + + chSms, chEmails := sendToLogger(sms, emails) + + logMessages(chEmails, chSms) + fmt.Println("===============================") +} + +func main() { + rand.Seed(0) + test( + []string{ + "hi friend", + "What's going on?", + "Welcome to the business", + "I'll pay you to be my friend", + }, + []string{ + "Will you make your appointment?", + "Let's be friends", + "What are you doing?", + "I can't believe you've done this.", + }, + ) + test( + []string{ + "this song slaps hard", + "yooo hoooo", + "i'm a big fan", + }, + []string{ + "What do you think of this song?", + "I hate this band", + "Can you believe this song?", + }, + ) +} + +func sendToLogger(sms, emails []string) (chSms, chEmails chan string) { + chSms = make(chan string) + chEmails = make(chan string) + go func() { + for i := 0; i < len(sms) && i < len(emails); i++ { + done := make(chan struct{}) + s := sms[i] + e := emails[i] + t1 := time.Millisecond * time.Duration(rand.Intn(1000)) + t2 := time.Millisecond * time.Duration(rand.Intn(1000)) + go func() { + time.Sleep(t1) + chSms <- s + done <- struct{}{} + }() + go func() { + time.Sleep(t2) + chEmails <- e + done <- struct{}{} + }() + <-done + <-done + time.Sleep(10 * time.Millisecond) + } + close(chSms) + close(chEmails) + }() + return chSms, chEmails +} diff --git a/course/13-channels/exercises/7-select/expected.txt b/course/13-channels/exercises/7-select/expected.txt new file mode 100644 index 0000000..99ece3d --- /dev/null +++ b/course/13-channels/exercises/7-select/expected.txt @@ -0,0 +1,18 @@ +Starting... +SMS: hi friend +Email: Will you make your appointment? +SMS: What's going on? +Email: Let's be friends +Email: What are you doing? +SMS: Welcome to the business +Email: I can't believe you've done this. +SMS: I'll pay you to be my friend +=============================== +Starting... +SMS: this song slaps hard +Email: What do you think of this song? +Email: I hate this band +SMS: yooo hoooo +SMS: i'm a big fan +Email: Can you believe this song? +=============================== diff --git a/course/13-channels/exercises/7-select/readme.md b/course/13-channels/exercises/7-select/readme.md new file mode 100644 index 0000000..da08e05 --- /dev/null +++ b/course/13-channels/exercises/7-select/readme.md @@ -0,0 +1,24 @@ +# Select + +Sometimes we have a single goroutine listening to multiple channels and want to process data in the order it comes through each channel. + +A `select` statement is used to listen to multiple channels at the same time. It is similar to a `switch` statement but for channels. + +```go +select { + case i, ok := <- chInts: + fmt.Println(i) + case s, ok := <- chStrings: + fmt.Println(s) +} +``` + +The first channel with a value ready to be received will fire and its body will execute. If multiple channels are ready at the same time one is chosen randomly. The `ok` variable in the example above refers to whether or not the channel has been closed by the sender yet. + +## Assignment + +Complete the `logMessages` function. + +Use an infinite `for` loop and a select statement to log the emails and sms messages as they come in order across the two channels. Add a condition to `return` from the function when *one* of the two channels closes, whichever is first. + +Use the `logSms` and `logEmail` functions to log the messages. diff --git a/course/13-channels/exercises/8-select_default/code.go b/course/13-channels/exercises/8-select_default/code.go new file mode 100644 index 0000000..744312c --- /dev/null +++ b/course/13-channels/exercises/8-select_default/code.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "time" +) + +func saveBackups(snapshotTicker, saveAfter <-chan time.Time) { + // ? +} + +// TEST SUITE - Don't touch below this line + +func takeSnapshot() { + fmt.Println("Taking a backup snapshot...") +} + +func saveSnapshot() { + fmt.Println("All backups saved!") +} + +func waitForData() { + fmt.Println("Nothing to do, waiting...") +} + +func test() { + snapshotTicker := time.Tick(800 * time.Millisecond) + saveAfter := time.After(2800 * time.Millisecond) + saveBackups(snapshotTicker, saveAfter) + fmt.Println("===========================") +} + +func main() { + test() +} diff --git a/course/13-channels/exercises/8-select_default/complete.go b/course/13-channels/exercises/8-select_default/complete.go new file mode 100644 index 0000000..05908af --- /dev/null +++ b/course/13-channels/exercises/8-select_default/complete.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "time" +) + +func saveBackups(snapshotTicker, saveAfter <-chan time.Time) { + for { + select { + case <-snapshotTicker: + takeSnapshot() + case <-saveAfter: + saveSnapshot() + return + default: + waitForData() + time.Sleep(500 * time.Millisecond) + } + } +} + +// TEST SUITE - Don't touch below this line + +func takeSnapshot() { + fmt.Println("Taking a backup snapshot...") +} + +func saveSnapshot() { + fmt.Println("All backups saved!") +} + +func waitForData() { + fmt.Println("Nothing to do, waiting...") +} + +func test() { + snapshotTicker := time.Tick(800 * time.Millisecond) + saveAfter := time.After(2800 * time.Millisecond) + saveBackups(snapshotTicker, saveAfter) + fmt.Println("===========================") +} + +func main() { + test() +} diff --git a/course/13-channels/exercises/8-select_default/expected.txt b/course/13-channels/exercises/8-select_default/expected.txt new file mode 100644 index 0000000..a9c401a --- /dev/null +++ b/course/13-channels/exercises/8-select_default/expected.txt @@ -0,0 +1,11 @@ +Nothing to do, waiting... +Nothing to do, waiting... +Taking a backup snapshot... +Nothing to do, waiting... +Nothing to do, waiting... +Taking a backup snapshot... +Nothing to do, waiting... +Taking a backup snapshot... +Nothing to do, waiting... +All backups saved! +=========================== diff --git a/course/13-channels/exercises/8-select_default/readme.md b/course/13-channels/exercises/8-select_default/readme.md new file mode 100644 index 0000000..4d49320 --- /dev/null +++ b/course/13-channels/exercises/8-select_default/readme.md @@ -0,0 +1,58 @@ +# Select Default Case + +The `default` case in a `select` statement executes *immediately* if no other channel has a value ready. A `default` case stops the `select` statement from blocking. + +```go +select { + case v := <-ch: + // use v + default: + // receiving from ch would block + // so do something else +} +``` + +## Tickers + +* [time.Tick()](https://golang.org/pkg/time/#Tick) is a standard library function that returns a channel that sends a value on a given interval. +* [time.After()](https://golang.org/pkg/time/#After) sends a value once after the duration has passed. +* [time.Sleep()](https://golang.org/pkg/time/#Sleep) blocks the current goroutine for the specified amount of time. + +## Read-only Channels + +A channel can be marked as read-only by casting it from a `chan` to a `<-chan` type. For example: + +```go +func main(){ + ch := make(chan int) + readCh(ch) +} + +func readCh(ch <-chan int) { + // ch can only be read from + // in this function +} +``` + +## Write-only Channels + +The same goes for write-only channels, but the arrow's position moves. + +```go +func writeCh(ch chan<- int) { + // ch can only be written to + // in this function +} +``` + +## Assignment + +Like all good back-end engineers, we frequently save backup snapshots of the Mailio database. + +Complete the `saveBackups` function. + +It should read values from the `snapshotTicker` and `saveAfter` channels simultaneously. + +* If a value is received from `snapshotTicker`, call `takeSnapshot()` +* If a value is received from `saveAfter`, call `saveSnapshot()` and `return` from the function: you're done. +* If neither channel has a value ready, call `waitForData()` and then [time.Sleep()](https://pkg.go.dev/time#example-Sleeps) for 500 milliseconds. After all, we want to show in the logs that the snapshot service is running. diff --git a/course/13-channels/exercises/9-channel_quiz/multiple_choice.json b/course/13-channels/exercises/9-channel_quiz/multiple_choice.json new file mode 100644 index 0000000..78e253c --- /dev/null +++ b/course/13-channels/exercises/9-channel_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What happens when you read from a nil channel?", + "answers": [ + "The receiver will block forever", + "The sender will block forever", + "Panic" + ] +} diff --git a/course/13-channels/exercises/9-channel_quiz/readme.md b/course/13-channels/exercises/9-channel_quiz/readme.md new file mode 100644 index 0000000..e40e448 --- /dev/null +++ b/course/13-channels/exercises/9-channel_quiz/readme.md @@ -0,0 +1,33 @@ +# Channels Review + +Here are a few extra things you should understand about channels from [Dave Cheney's awesome article](https://dave.cheney.net/2014/03/19/channel-axioms). + +## A send to a nil channel blocks forever + +```go +var c chan string // c is nil +c <- "let's get started" // blocks +``` + +## A receive from a nil channel blocks forever + +```go +var c chan string // c is nil +fmt.Println(<-c) // blocks +``` + +## A send to a closed channel panics + +```go +var c = make(chan int, 100) +close(c) +c <- 1 // panic: send on closed channel +``` + +## A receive from a closed channel returns the zero value immediately + +```go +var c = make(chan int, 100) +close(c) +fmt.Println(<-c) // 0 +``` diff --git a/course/13-channels/exercises/9a-channel_quiz/multiple_choice.json b/course/13-channels/exercises/9a-channel_quiz/multiple_choice.json new file mode 100644 index 0000000..ab1ea03 --- /dev/null +++ b/course/13-channels/exercises/9a-channel_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What happens when you send to a closed channel?", + "answers": [ + "Panic", + "The sender will block forever", + "The receiver will block forever" + ] +} diff --git a/course/13-channels/exercises/9a-channel_quiz/readme.md b/course/13-channels/exercises/9a-channel_quiz/readme.md new file mode 100644 index 0000000..e40e448 --- /dev/null +++ b/course/13-channels/exercises/9a-channel_quiz/readme.md @@ -0,0 +1,33 @@ +# Channels Review + +Here are a few extra things you should understand about channels from [Dave Cheney's awesome article](https://dave.cheney.net/2014/03/19/channel-axioms). + +## A send to a nil channel blocks forever + +```go +var c chan string // c is nil +c <- "let's get started" // blocks +``` + +## A receive from a nil channel blocks forever + +```go +var c chan string // c is nil +fmt.Println(<-c) // blocks +``` + +## A send to a closed channel panics + +```go +var c = make(chan int, 100) +close(c) +c <- 1 // panic: send on closed channel +``` + +## A receive from a closed channel returns the zero value immediately + +```go +var c = make(chan int, 100) +close(c) +fmt.Println(<-c) // 0 +``` diff --git a/course/14-mutexes/exercises/1-mutex/code.go b/course/14-mutexes/exercises/1-mutex/code.go new file mode 100644 index 0000000..fa80d3f --- /dev/null +++ b/course/14-mutexes/exercises/1-mutex/code.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" + "sort" + "sync" + "time" +) + +type safeCounter struct { + counts map[string]int + mux *sync.Mutex +} + +func (sc safeCounter) inc(key string) { + sc.slowIncrement(key) +} + +func (sc safeCounter) val(key string) int { + return sc.counts[key] +} + +// don't touch below this line + +func (sc safeCounter) slowIncrement(key string) { + tempCounter := sc.counts[key] + time.Sleep(time.Microsecond) + tempCounter++ + sc.counts[key] = tempCounter +} + +type emailTest struct { + email string + count int +} + +func test(sc safeCounter, emailTests []emailTest) { + emails := make(map[string]struct{}) + + var wg sync.WaitGroup + for _, emailT := range emailTests { + emails[emailT.email] = struct{}{} + for i := 0; i < emailT.count; i++ { + wg.Add(1) + go func(emailT emailTest) { + sc.inc(emailT.email) + wg.Done() + }(emailT) + } + } + wg.Wait() + + emailsSorted := make([]string, 0, len(emails)) + for email := range emails { + emailsSorted = append(emailsSorted, email) + } + sort.Strings(emailsSorted) + + for _, email := range emailsSorted { + fmt.Printf("Email: %s has %d emails\n", email, sc.val(email)) + } + fmt.Println("=====================================") +} + +func main() { + sc := safeCounter{ + counts: make(map[string]int), + mux: &sync.Mutex{}, + } + test(sc, []emailTest{ + { + email: "john@example.com", + count: 23, + }, + { + email: "john@example.com", + count: 29, + }, + { + email: "jill@example.com", + count: 31, + }, + { + email: "jill@example.com", + count: 67, + }, + }) + test(sc, []emailTest{ + { + email: "kaden@example.com", + count: 23, + }, + { + email: "george@example.com", + count: 126, + }, + { + email: "kaden@example.com", + count: 31, + }, + { + email: "george@example.com", + count: 453, + }, + }) +} diff --git a/course/14-mutexes/exercises/1-mutex/complete.go b/course/14-mutexes/exercises/1-mutex/complete.go new file mode 100644 index 0000000..723e13a --- /dev/null +++ b/course/14-mutexes/exercises/1-mutex/complete.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "sort" + "sync" + "time" +) + +type safeCounter struct { + counts map[string]int + mux *sync.Mutex +} + +func (sc safeCounter) inc(key string) { + sc.mux.Lock() + defer sc.mux.Unlock() + sc.slowIncrement(key) +} + +func (sc safeCounter) val(key string) int { + sc.mux.Lock() + defer sc.mux.Unlock() + return sc.counts[key] +} + +// don't touch below this line + +func (sc safeCounter) slowIncrement(key string) { + tempCounter := sc.counts[key] + time.Sleep(time.Microsecond) + tempCounter++ + sc.counts[key] = tempCounter +} + +type emailTest struct { + email string + count int +} + +func test(sc safeCounter, emailTests []emailTest) { + emails := make(map[string]struct{}) + + var wg sync.WaitGroup + for _, emailT := range emailTests { + emails[emailT.email] = struct{}{} + for i := 0; i < emailT.count; i++ { + wg.Add(1) + go func(emailT emailTest) { + sc.inc(emailT.email) + wg.Done() + }(emailT) + } + } + wg.Wait() + + emailsSorted := make([]string, 0, len(emails)) + for email := range emails { + emailsSorted = append(emailsSorted, email) + } + sort.Strings(emailsSorted) + + for _, email := range emailsSorted { + fmt.Printf("Email: %s has %d emails\n", email, sc.val(email)) + } + fmt.Println("=====================================") +} + +func main() { + sc := safeCounter{ + counts: make(map[string]int), + mux: &sync.Mutex{}, + } + test(sc, []emailTest{ + { + email: "john@example.com", + count: 23, + }, + { + email: "john@example.com", + count: 29, + }, + { + email: "jill@example.com", + count: 31, + }, + { + email: "jill@example.com", + count: 67, + }, + }) + test(sc, []emailTest{ + { + email: "kaden@example.com", + count: 23, + }, + { + email: "george@example.com", + count: 126, + }, + { + email: "kaden@example.com", + count: 31, + }, + { + email: "george@example.com", + count: 453, + }, + }) +} diff --git a/course/14-mutexes/exercises/1-mutex/expected.txt b/course/14-mutexes/exercises/1-mutex/expected.txt new file mode 100644 index 0000000..78b2275 --- /dev/null +++ b/course/14-mutexes/exercises/1-mutex/expected.txt @@ -0,0 +1,6 @@ +Email: jill@example.com has 98 emails +Email: john@example.com has 52 emails +===================================== +Email: george@example.com has 579 emails +Email: kaden@example.com has 54 emails +===================================== diff --git a/course/14-mutexes/exercises/1-mutex/readme.md b/course/14-mutexes/exercises/1-mutex/readme.md new file mode 100644 index 0000000..be0aa9f --- /dev/null +++ b/course/14-mutexes/exercises/1-mutex/readme.md @@ -0,0 +1,39 @@ +# Mutexes in Go + +Mutexes allow us to *lock* access to data. This ensures that we can control which goroutines can access certain data at which time. + +Go's standard library provides a built-in implementation of a mutex with the [sync.Mutex](https://pkg.go.dev/sync#Mutex) type and its two methods: + +* [.Lock()](https://golang.org/pkg/sync/#Mutex.Lock) +* [.Unlock()](https://golang.org/pkg/sync/#Mutex.Unlock) + +We can protect a block of code by surrounding it with a call to `Lock` and `Unlock` as shown on the `protected()` method below. + +It's good practice to structure the protected code within a function so that `defer` can be used to ensure that we never forget to unlock the mutex. + +```go +func protected(){ + mux.Lock() + defer mux.Unlock() + // the rest of the function is protected + // any other calls to `mux.Lock()` will block +} +``` + +Mutexes are powerful. Like most powerful things, they can also cause many bugs if used carelessly. + +## Maps are not thread-safe + +Maps are **not** safe for concurrent use! If you have multiple goroutines accessing the same map, and at least one of them is writing to the map, you must lock your maps with a mutex. + +## Assignment + +We send emails across many different goroutines at Mailio. To keep track of how many we've sent to a given email address, we use an in-memory map. + +Our `safeCounter` struct is unsafe! Update the `inc()` and `val()` methods so that they utilize the safeCounter's mutex to ensure that the map is not accessed by multiple goroutines at the same time. + +## Note: WASM is single-threaded + +Now, it's worth pointing out that our execution engine on Boot.dev uses web assembly to run the code you write in your browser. Web assembly is single-threaded, which awkwardly means that maps *are* thread-safe in web assembly. I've *simulated* a multi-threaded environment with the `slowIncrement` function. + +In reality, any Go code you write *may or may not* run on a single-core machine, so it's always best to write your code so that it is safe no matter which hardware it runs on. diff --git a/course/14-mutexes/exercises/3-mutex_name/multiple_choice.json b/course/14-mutexes/exercises/3-mutex_name/multiple_choice.json new file mode 100644 index 0000000..0653525 --- /dev/null +++ b/course/14-mutexes/exercises/3-mutex_name/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What does the term 'mutex' refer to?", + "answers": [ + "Mutual Exclusion", + "Most Expressive", + "More Expansive", + "Mutual Expulsion" + ] +} diff --git a/course/14-mutexes/exercises/3-mutex_name/readme.md b/course/14-mutexes/exercises/3-mutex_name/readme.md new file mode 100644 index 0000000..ea6ccff --- /dev/null +++ b/course/14-mutexes/exercises/3-mutex_name/readme.md @@ -0,0 +1,5 @@ +# Why is it called a "mutex"? + +Mutex is short for [mutual exclusion](https://en.wikipedia.org/wiki/Mutual_exclusion), and the conventional name for the data structure that provides it is "mutex", often abbreviated to "mux". + +It's called "mutual exclusion" because a mutex *excludes* different threads (or goroutines) from accessing the same data at the same time. diff --git a/course/14-mutexes/exercises/4-mutex_review/multiple_choice.json b/course/14-mutexes/exercises/4-mutex_review/multiple_choice.json new file mode 100644 index 0000000..cd2bf8d --- /dev/null +++ b/course/14-mutexes/exercises/4-mutex_review/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "How many threads can Lock a mutex at once?", + "answers": [ + "1", + "4", + "0", + "infinite" + ] +} diff --git a/course/14-mutexes/exercises/4-mutex_review/readme.md b/course/14-mutexes/exercises/4-mutex_review/readme.md new file mode 100644 index 0000000..801d903 --- /dev/null +++ b/course/14-mutexes/exercises/4-mutex_review/readme.md @@ -0,0 +1,97 @@ +# Mutex Review + +The principle problem that mutexes help us avoid is the *concurrent read/write problem*. This problem arises when one thread is writing to a variable while another thread is reading from that same variable *at the same time*. + +When this happens, a Go program will panic because the reader could be reading bad data while it's being mutated in place. + +![mutex](https://i.imgur.com/NGBnMXe.png) + +## Mutex example + +```go +package main + +import ( + "fmt" +) + +func main() { + m := map[int]int{} + go writeLoop(m) + go readLoop(m) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int) { + for { + for i := 0; i < 100; i++ { + m[i] = i + } + } +} + +func readLoop(m map[int]int) { + for { + for k, v := range m { + fmt.Println(k, "-", v) + } + } +} +``` + +The example above creates a map, then starts two goroutines which each have access to the map. One goroutine continuously mutates the values stored in the map, while the other prints the values it finds in the map. + +If we run the program on a multi-core machine, we get the following output: `fatal error: concurrent map iteration and map write` + +In Go, it isn’t safe to read from and write to a map at the same time. + +## Mutexes to the rescue + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + m := map[int]int{} + + mux := &sync.Mutex{} + + go writeLoop(m, mux) + go readLoop(m, mux) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int, mux *sync.Mutex) { + for { + for i := 0; i < 100; i++ { + mux.Lock() + m[i] = i + mux.Unlock() + } + } +} + +func readLoop(m map[int]int, mux *sync.Mutex) { + for { + mux.Lock() + for k, v := range m { + fmt.Println(k, "-", v) + } + mux.Unlock() + } +} +``` + +In this example, we added a `sync.Mutex{}` and named it `mux`. In the write loop, the `Lock()` method is called before writing, and then the `Unlock()` is called when we're done. This Lock/Unlock sequence ensures that no other threads can `Lock()` the mutex while *we* have it locked – any other threads attempting to `Lock()` will block and wait until we `Unlock()`. + +In the reader, we `Lock()` before iterating over the map, and likewise `Unlock()` when we're done. Now the threads share the memory safely! diff --git a/course/14-mutexes/exercises/4a-mutex_review/multiple_choice.json b/course/14-mutexes/exercises/4a-mutex_review/multiple_choice.json new file mode 100644 index 0000000..941a89c --- /dev/null +++ b/course/14-mutexes/exercises/4a-mutex_review/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Why would you use a mutex?", + "answers": [ + "To safely access a data structure concurrently", + "To protect data from network access", + "To stop other packages from using my code", + "To encapsulate private data members of a struct" + ] +} diff --git a/course/14-mutexes/exercises/4a-mutex_review/readme.md b/course/14-mutexes/exercises/4a-mutex_review/readme.md new file mode 100644 index 0000000..801d903 --- /dev/null +++ b/course/14-mutexes/exercises/4a-mutex_review/readme.md @@ -0,0 +1,97 @@ +# Mutex Review + +The principle problem that mutexes help us avoid is the *concurrent read/write problem*. This problem arises when one thread is writing to a variable while another thread is reading from that same variable *at the same time*. + +When this happens, a Go program will panic because the reader could be reading bad data while it's being mutated in place. + +![mutex](https://i.imgur.com/NGBnMXe.png) + +## Mutex example + +```go +package main + +import ( + "fmt" +) + +func main() { + m := map[int]int{} + go writeLoop(m) + go readLoop(m) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int) { + for { + for i := 0; i < 100; i++ { + m[i] = i + } + } +} + +func readLoop(m map[int]int) { + for { + for k, v := range m { + fmt.Println(k, "-", v) + } + } +} +``` + +The example above creates a map, then starts two goroutines which each have access to the map. One goroutine continuously mutates the values stored in the map, while the other prints the values it finds in the map. + +If we run the program on a multi-core machine, we get the following output: `fatal error: concurrent map iteration and map write` + +In Go, it isn’t safe to read from and write to a map at the same time. + +## Mutexes to the rescue + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + m := map[int]int{} + + mux := &sync.Mutex{} + + go writeLoop(m, mux) + go readLoop(m, mux) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int, mux *sync.Mutex) { + for { + for i := 0; i < 100; i++ { + mux.Lock() + m[i] = i + mux.Unlock() + } + } +} + +func readLoop(m map[int]int, mux *sync.Mutex) { + for { + mux.Lock() + for k, v := range m { + fmt.Println(k, "-", v) + } + mux.Unlock() + } +} +``` + +In this example, we added a `sync.Mutex{}` and named it `mux`. In the write loop, the `Lock()` method is called before writing, and then the `Unlock()` is called when we're done. This Lock/Unlock sequence ensures that no other threads can `Lock()` the mutex while *we* have it locked – any other threads attempting to `Lock()` will block and wait until we `Unlock()`. + +In the reader, we `Lock()` before iterating over the map, and likewise `Unlock()` when we're done. Now the threads share the memory safely! diff --git a/course/14-mutexes/exercises/5-rw_mutex/code.go b/course/14-mutexes/exercises/5-rw_mutex/code.go new file mode 100644 index 0000000..4f990e7 --- /dev/null +++ b/course/14-mutexes/exercises/5-rw_mutex/code.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "sort" + "sync" + "time" +) + +type safeCounter struct { + counts map[string]int + mux *sync.RWMutex +} + +func (sc safeCounter) inc(key string) { + sc.mux.Lock() + defer sc.mux.Unlock() + sc.slowIncrement(key) +} + +func (sc safeCounter) val(key string) int { + sc.mux.Lock() + defer sc.mux.Unlock() + return sc.counts[key] +} + +// don't touch below this line + +func (sc safeCounter) slowIncrement(key string) { + tempCounter := sc.counts[key] + time.Sleep(time.Microsecond) + tempCounter++ + sc.counts[key] = tempCounter +} + +type emailTest struct { + email string + count int +} + +func test(sc safeCounter, emailTests []emailTest) { + emails := make(map[string]struct{}) + + var wg sync.WaitGroup + for _, emailT := range emailTests { + emails[emailT.email] = struct{}{} + for i := 0; i < emailT.count; i++ { + wg.Add(1) + go func(emailT emailTest) { + sc.inc(emailT.email) + wg.Done() + }(emailT) + } + } + wg.Wait() + + emailsSorted := make([]string, 0, len(emails)) + for email := range emails { + emailsSorted = append(emailsSorted, email) + } + sort.Strings(emailsSorted) + + sc.mux.RLock() + defer sc.mux.RUnlock() + for _, email := range emailsSorted { + fmt.Printf("Email: %s has %d emails\n", email, sc.val(email)) + } + fmt.Println("=====================================") +} + +func main() { + sc := safeCounter{ + counts: make(map[string]int), + mux: &sync.RWMutex{}, + } + test(sc, []emailTest{ + { + email: "john@example.com", + count: 23, + }, + { + email: "john@example.com", + count: 29, + }, + { + email: "jill@example.com", + count: 31, + }, + { + email: "jill@example.com", + count: 67, + }, + }) + test(sc, []emailTest{ + { + email: "kaden@example.com", + count: 23, + }, + { + email: "george@example.com", + count: 126, + }, + { + email: "kaden@example.com", + count: 31, + }, + { + email: "george@example.com", + count: 453, + }, + }) +} diff --git a/course/14-mutexes/exercises/5-rw_mutex/complete.go b/course/14-mutexes/exercises/5-rw_mutex/complete.go new file mode 100644 index 0000000..f05bd8d --- /dev/null +++ b/course/14-mutexes/exercises/5-rw_mutex/complete.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "sort" + "sync" + "time" +) + +type safeCounter struct { + counts map[string]int + mux *sync.RWMutex +} + +func (sc safeCounter) inc(key string) { + sc.mux.Lock() + defer sc.mux.Unlock() + sc.slowIncrement(key) +} + +func (sc safeCounter) val(key string) int { + sc.mux.RLock() + defer sc.mux.RUnlock() + return sc.counts[key] +} + +// don't touch below this line + +func (sc safeCounter) slowIncrement(key string) { + tempCounter := sc.counts[key] + time.Sleep(time.Microsecond) + tempCounter++ + sc.counts[key] = tempCounter +} + +type emailTest struct { + email string + count int +} + +func test(sc safeCounter, emailTests []emailTest) { + emails := make(map[string]struct{}) + + var wg sync.WaitGroup + for _, emailT := range emailTests { + emails[emailT.email] = struct{}{} + for i := 0; i < emailT.count; i++ { + wg.Add(1) + go func(emailT emailTest) { + sc.inc(emailT.email) + wg.Done() + }(emailT) + } + } + wg.Wait() + + emailsSorted := make([]string, 0, len(emails)) + for email := range emails { + emailsSorted = append(emailsSorted, email) + } + sort.Strings(emailsSorted) + + sc.mux.RLock() + defer sc.mux.RUnlock() + for _, email := range emailsSorted { + fmt.Printf("Email: %s has %d emails\n", email, sc.val(email)) + } + fmt.Println("=====================================") +} + +func main() { + sc := safeCounter{ + counts: make(map[string]int), + mux: &sync.RWMutex{}, + } + test(sc, []emailTest{ + { + email: "john@example.com", + count: 23, + }, + { + email: "john@example.com", + count: 29, + }, + { + email: "jill@example.com", + count: 31, + }, + { + email: "jill@example.com", + count: 67, + }, + }) + test(sc, []emailTest{ + { + email: "kaden@example.com", + count: 23, + }, + { + email: "george@example.com", + count: 126, + }, + { + email: "kaden@example.com", + count: 31, + }, + { + email: "george@example.com", + count: 453, + }, + }) +} diff --git a/course/14-mutexes/exercises/5-rw_mutex/expected.txt b/course/14-mutexes/exercises/5-rw_mutex/expected.txt new file mode 100644 index 0000000..78b2275 --- /dev/null +++ b/course/14-mutexes/exercises/5-rw_mutex/expected.txt @@ -0,0 +1,6 @@ +Email: jill@example.com has 98 emails +Email: john@example.com has 52 emails +===================================== +Email: george@example.com has 579 emails +Email: kaden@example.com has 54 emails +===================================== diff --git a/course/14-mutexes/exercises/5-rw_mutex/readme.md b/course/14-mutexes/exercises/5-rw_mutex/readme.md new file mode 100644 index 0000000..4716b7a --- /dev/null +++ b/course/14-mutexes/exercises/5-rw_mutex/readme.md @@ -0,0 +1,21 @@ +# RW Mutex + +The standard library also exposes a [sync.RWMutex](https://golang.org/pkg/sync/#RWMutex) + +In addition to these methods: + +* [Lock()](https://golang.org/pkg/sync/#Mutex.Lock) +* [Unlock()](https://golang.org/pkg/sync/#Mutex.Unlock) + +The `sync.RWMutex` also has these methods: + +* [RLock()](https://golang.org/pkg/sync/#RWMutex.RLock) +* [RUnlock()](https://golang.org/pkg/sync/#RWMutex.RUnlock) + +The `sync.RWMutex` can help with performance if we have a read-intensive process. Many goroutines can safely read from the map at the same time (multiple `Rlock()` calls can happen simultaneously). However, only one goroutine can hold a `Lock()` and all `RLock()`'s will also be excluded. + +## Assignment + +Let's update our same code from the last assignment, but this time we can speed it up by allowing readers to read from the map concurrently. + +Update the `val` method to only lock the mutex for *reading*. Notice that if you run the code with a write lock it will block forever. diff --git a/course/14-mutexes/exercises/6-rw_mutex_review/multiple_choice.json b/course/14-mutexes/exercises/6-rw_mutex_review/multiple_choice.json new file mode 100644 index 0000000..0f01799 --- /dev/null +++ b/course/14-mutexes/exercises/6-rw_mutex_review/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "How many writers can access a RWMutex at once?", + "answers": [ + "1", + "0", + "infinite" + ] +} diff --git a/course/14-mutexes/exercises/6-rw_mutex_review/readme.md b/course/14-mutexes/exercises/6-rw_mutex_review/readme.md new file mode 100644 index 0000000..7ddc3c1 --- /dev/null +++ b/course/14-mutexes/exercises/6-rw_mutex_review/readme.md @@ -0,0 +1,51 @@ +# Read/Write Mutex Review + +Maps are safe for concurrent *read* access, just not concurrent read/write or write/write access. A read/write mutex allows all the readers to access the map at the same time, but a writer will still lock out all other readers and writers. + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + m := map[int]int{} + + mux := &sync.RWMutex{} + + go writeLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int, mux *sync.RWMutex) { + for { + for i := 0; i < 100; i++ { + mux.Lock() + m[i] = i + mux.Unlock() + } + } +} + +func readLoop(m map[int]int, mux *sync.RWMutex) { + for { + mux.RLock() + for k, v := range m { + fmt.Println(k, "-", v) + } + mux.RUnlock() + } +} +``` + +By using a `sync.RWMutex`, our program becomes *more efficient*. We can have as many `readLoop()` threads as we want, while still ensuring that the writers have exclusive access. + diff --git a/course/14-mutexes/exercises/6a-rw_mutex_review/multiple_choice.json b/course/14-mutexes/exercises/6a-rw_mutex_review/multiple_choice.json new file mode 100644 index 0000000..365a70e --- /dev/null +++ b/course/14-mutexes/exercises/6a-rw_mutex_review/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "How many readers can access a RWMutex at once?", + "answers": [ + "infinite", + "1", + "0" + ] +} diff --git a/course/14-mutexes/exercises/6a-rw_mutex_review/readme.md b/course/14-mutexes/exercises/6a-rw_mutex_review/readme.md new file mode 100644 index 0000000..7ddc3c1 --- /dev/null +++ b/course/14-mutexes/exercises/6a-rw_mutex_review/readme.md @@ -0,0 +1,51 @@ +# Read/Write Mutex Review + +Maps are safe for concurrent *read* access, just not concurrent read/write or write/write access. A read/write mutex allows all the readers to access the map at the same time, but a writer will still lock out all other readers and writers. + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + m := map[int]int{} + + mux := &sync.RWMutex{} + + go writeLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int, mux *sync.RWMutex) { + for { + for i := 0; i < 100; i++ { + mux.Lock() + m[i] = i + mux.Unlock() + } + } +} + +func readLoop(m map[int]int, mux *sync.RWMutex) { + for { + mux.RLock() + for k, v := range m { + fmt.Println(k, "-", v) + } + mux.RUnlock() + } +} +``` + +By using a `sync.RWMutex`, our program becomes *more efficient*. We can have as many `readLoop()` threads as we want, while still ensuring that the writers have exclusive access. + diff --git a/course/14-mutexes/exercises/6b-rw_mutex_review/multiple_choice.json b/course/14-mutexes/exercises/6b-rw_mutex_review/multiple_choice.json new file mode 100644 index 0000000..f459273 --- /dev/null +++ b/course/14-mutexes/exercises/6b-rw_mutex_review/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Can readers and writers use RWMutexes at the same time?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/14-mutexes/exercises/6b-rw_mutex_review/readme.md b/course/14-mutexes/exercises/6b-rw_mutex_review/readme.md new file mode 100644 index 0000000..7ddc3c1 --- /dev/null +++ b/course/14-mutexes/exercises/6b-rw_mutex_review/readme.md @@ -0,0 +1,51 @@ +# Read/Write Mutex Review + +Maps are safe for concurrent *read* access, just not concurrent read/write or write/write access. A read/write mutex allows all the readers to access the map at the same time, but a writer will still lock out all other readers and writers. + +```go +package main + +import ( + "fmt" + "sync" +) + +func main() { + m := map[int]int{} + + mux := &sync.RWMutex{} + + go writeLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + go readLoop(m, mux) + + // stop program from exiting, must be killed + block := make(chan struct{}) + <-block +} + +func writeLoop(m map[int]int, mux *sync.RWMutex) { + for { + for i := 0; i < 100; i++ { + mux.Lock() + m[i] = i + mux.Unlock() + } + } +} + +func readLoop(m map[int]int, mux *sync.RWMutex) { + for { + mux.RLock() + for k, v := range m { + fmt.Println(k, "-", v) + } + mux.RUnlock() + } +} +``` + +By using a `sync.RWMutex`, our program becomes *more efficient*. We can have as many `readLoop()` threads as we want, while still ensuring that the writers have exclusive access. + diff --git a/course/15-generics/exercises/1-generics/code.go b/course/15-generics/exercises/1-generics/code.go new file mode 100644 index 0000000..66bf3e0 --- /dev/null +++ b/course/15-generics/exercises/1-generics/code.go @@ -0,0 +1,74 @@ +package main + +import "fmt" + +func getLast[]() { + +} + +// don't edit below this line + +type email struct { + message string + senderEmail string + recipientEmail string +} + +type payment struct { + amount int + senderEmail string + recipientEmail string +} + +func main() { + test([]email{}, "email") + test([]email{ + { + "Hi Margo", + "janet@example.com", + "margo@example.com", + }, + { + "Hey Margo I really wanna chat", + "janet@example.com", + "margo@example.com", + }, + { + "ANSWER ME", + "janet@example.com", + "margo@example.com", + }, + }, "email") + test([]payment{ + { + 5, + "jane@example.com", + "sally@example.com", + }, + { + 25, + "jane@example.com", + "mark@example.com", + }, + { + 1, + "jane@example.com", + "sally@example.com", + }, + { + 16, + "jane@example.com", + "margo@example.com", + }, + }, "payment") +} + +func test[T any](s []T, desc string) { + last := getLast(s) + fmt.Printf("Getting last %v from slice of length: %v\n", desc, len(s)) + for i, v := range s { + fmt.Printf("Item #%v: %v\n", i+1, v) + } + fmt.Printf("Last item in list: %v\n", last) + fmt.Println(" --- ") +} diff --git a/course/15-generics/exercises/1-generics/complete.go b/course/15-generics/exercises/1-generics/complete.go new file mode 100644 index 0000000..54bb6b4 --- /dev/null +++ b/course/15-generics/exercises/1-generics/complete.go @@ -0,0 +1,78 @@ +package main + +import "fmt" + +func getLast[T any](s []T) T { + if len(s) == 0 { + var zero T + return zero + } + return s[len(s)-1] +} + +// don't edit below this line + +type email struct { + message string + senderEmail string + recipientEmail string +} + +type payment struct { + amount int + senderEmail string + recipientEmail string +} + +func main() { + test([]email{}, "email") + test([]email{ + { + "Hi Margo", + "janet@example.com", + "margo@example.com", + }, + { + "Hey Margo I really wanna chat", + "janet@example.com", + "margo@example.com", + }, + { + "ANSWER ME", + "janet@example.com", + "margo@example.com", + }, + }, "email") + test([]payment{ + { + 5, + "jane@example.com", + "sally@example.com", + }, + { + 25, + "jane@example.com", + "mark@example.com", + }, + { + 1, + "jane@example.com", + "sally@example.com", + }, + { + 16, + "jane@example.com", + "margo@example.com", + }, + }, "payment") +} + +func test[T any](s []T, desc string) { + last := getLast(s) + fmt.Printf("Getting last %v from slice of length: %v\n", desc, len(s)) + for i, v := range s { + fmt.Printf("Item #%v: %v\n", i+1, v) + } + fmt.Printf("Last item in list: %v\n", last) + fmt.Println(" --- ") +} diff --git a/course/15-generics/exercises/1-generics/expected.txt b/course/15-generics/exercises/1-generics/expected.txt new file mode 100644 index 0000000..4528f5f --- /dev/null +++ b/course/15-generics/exercises/1-generics/expected.txt @@ -0,0 +1,16 @@ +Getting last email from slice of length: 0 +Last item in list: { } + --- +Getting last email from slice of length: 3 +Item #1: {Hi Margo janet@example.com margo@example.com} +Item #2: {Hey Margo I really wanna chat janet@example.com margo@example.com} +Item #3: {ANSWER ME janet@example.com margo@example.com} +Last item in list: {ANSWER ME janet@example.com margo@example.com} + --- +Getting last payment from slice of length: 4 +Item #1: {5 jane@example.com sally@example.com} +Item #2: {25 jane@example.com mark@example.com} +Item #3: {1 jane@example.com sally@example.com} +Item #4: {16 jane@example.com margo@example.com} +Last item in list: {16 jane@example.com margo@example.com} + --- diff --git a/course/15-generics/exercises/1-generics/readme.md b/course/15-generics/exercises/1-generics/readme.md new file mode 100644 index 0000000..6b718f1 --- /dev/null +++ b/course/15-generics/exercises/1-generics/readme.md @@ -0,0 +1,57 @@ +# Generics in Go + +As we've mentioned, Go does *not* support classes. For a long time, that meant that Go code couldn't easily be reused in many circumstances. For example, imagine some code that splits a slice into 2 equal parts. The code that splits the slice doesn't really care about the *values* stored in the slice. Unfortunately in Go we would need to write it multiple times for each type, which is a very un-[DRY](https://blog.boot.dev/clean-code/dry-code/) thing to do. + +```go +func splitIntSlice(s []int) ([]int, []int) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` + +```go +func splitStringSlice(s []string) ([]string, []string) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` + +In Go 1.20 however, support for [generics](https://blog.boot.dev/golang/how-to-use-golangs-generics/) was released, effectively solving this problem! + +## Type Parameters + +Put simply, generics allow us to use variables to refer to specific types. This is an amazing feature because it allows us to write abstract functions that drastically reduce code duplication. + +```go +func splitAnySlice[T any](s []T) ([]T, []T) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` + +In the example above, `T` is the name of the type parameter for the `splitAnySlice` function, and we've said that it must match the `any` constraint, which means it can be anything. This makes sense because the body of the function *doesn't care* about the types of things stored in the slice. + +```go +firstInts, secondInts := splitAnySlice([]int{0, 1, 2, 3}) +fmt.Println(firstInts, secondInts) +``` + +## Assignment + +At Mailio we often store all the emails of a given email campaign in memory as a slice. We store payments for a single user in the same way. + +Complete the `getLast()` function. It should be a generic function that returns the last element from a slice, no matter the types stored in the slice. If the slice is empty, it should return the zero value of the type. + +## Tip: Zero value of a type + +Creating a variable that's the zero value of a type is easy: + +```go +var myZeroInt int +``` + +It's the same with generics, we just have a variable that represents the type: + +```go +var myZero T +``` diff --git a/course/15-generics/exercises/2-generics_why/multiple_choice.json b/course/15-generics/exercises/2-generics_why/multiple_choice.json new file mode 100644 index 0000000..fd1bd35 --- /dev/null +++ b/course/15-generics/exercises/2-generics_why/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Which code would generics be most likely to help with?", + "answers": [ + "A binary tree", + "Calculating the area of a circle", + "Detecting whether or not a string contains a given substring" + ] +} diff --git a/course/15-generics/exercises/2-generics_why/readme.md b/course/15-generics/exercises/2-generics_why/readme.md new file mode 100644 index 0000000..af08d5b --- /dev/null +++ b/course/15-generics/exercises/2-generics_why/readme.md @@ -0,0 +1,15 @@ +# Why Generics? + +## Generics reduce repetitive code + +You should care about generics because they mean you don’t have to write as much code! It can be frustrating to write the same logic over and over again, just because you have some underlying data types that are slightly different. + +## Generics are used more often in libraries and packages + +Generics give Go developers an elegant way to write amazing utility packages. While you will see and use generics in application code, I think it will much more common to see generics used in libraries and packages. Libraries and packages contain importable code intended to be used in *many* applications, so it makes sense to write them in a more abstract way. Generics are often the way to do just that! + +## Why did it take so long to get generics? + +Go places an emphasis on simplicity. In other words, Go has purposefully left out many features to provide its best feature: being simple and easy to work with. + +According to [historical data from Go surveys](https://go.dev/blog/survey2020-results), Go’s lack of generics has always been listed as one of the top three biggest issues with the language. At a certain point, the drawbacks associated with the lack of a feature like generics justify adding complexity to the language. diff --git a/course/15-generics/exercises/2a-generics_why/multiple_choice.json b/course/15-generics/exercises/2a-generics_why/multiple_choice.json new file mode 100644 index 0000000..3bf8df1 --- /dev/null +++ b/course/15-generics/exercises/2a-generics_why/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Go's approach to language design is...", + "answers": [ + "Resist adding new features unless they're extremely important", + "To support as many useful features as possible", + "To never add new features, the language doesn't change" + ] +} diff --git a/course/15-generics/exercises/2a-generics_why/readme.md b/course/15-generics/exercises/2a-generics_why/readme.md new file mode 100644 index 0000000..af08d5b --- /dev/null +++ b/course/15-generics/exercises/2a-generics_why/readme.md @@ -0,0 +1,15 @@ +# Why Generics? + +## Generics reduce repetitive code + +You should care about generics because they mean you don’t have to write as much code! It can be frustrating to write the same logic over and over again, just because you have some underlying data types that are slightly different. + +## Generics are used more often in libraries and packages + +Generics give Go developers an elegant way to write amazing utility packages. While you will see and use generics in application code, I think it will much more common to see generics used in libraries and packages. Libraries and packages contain importable code intended to be used in *many* applications, so it makes sense to write them in a more abstract way. Generics are often the way to do just that! + +## Why did it take so long to get generics? + +Go places an emphasis on simplicity. In other words, Go has purposefully left out many features to provide its best feature: being simple and easy to work with. + +According to [historical data from Go surveys](https://go.dev/blog/survey2020-results), Go’s lack of generics has always been listed as one of the top three biggest issues with the language. At a certain point, the drawbacks associated with the lack of a feature like generics justify adding complexity to the language. diff --git a/course/15-generics/exercises/2b-generics_why/multiple_choice.json b/course/15-generics/exercises/2b-generics_why/multiple_choice.json new file mode 100644 index 0000000..c0a6493 --- /dev/null +++ b/course/15-generics/exercises/2b-generics_why/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Generics will probably be used more heavily in...", + "answers": [ + "Library packages", + "Main packages (executable applications)" + ] +} diff --git a/course/15-generics/exercises/2b-generics_why/readme.md b/course/15-generics/exercises/2b-generics_why/readme.md new file mode 100644 index 0000000..af08d5b --- /dev/null +++ b/course/15-generics/exercises/2b-generics_why/readme.md @@ -0,0 +1,15 @@ +# Why Generics? + +## Generics reduce repetitive code + +You should care about generics because they mean you don’t have to write as much code! It can be frustrating to write the same logic over and over again, just because you have some underlying data types that are slightly different. + +## Generics are used more often in libraries and packages + +Generics give Go developers an elegant way to write amazing utility packages. While you will see and use generics in application code, I think it will much more common to see generics used in libraries and packages. Libraries and packages contain importable code intended to be used in *many* applications, so it makes sense to write them in a more abstract way. Generics are often the way to do just that! + +## Why did it take so long to get generics? + +Go places an emphasis on simplicity. In other words, Go has purposefully left out many features to provide its best feature: being simple and easy to work with. + +According to [historical data from Go surveys](https://go.dev/blog/survey2020-results), Go’s lack of generics has always been listed as one of the top three biggest issues with the language. At a certain point, the drawbacks associated with the lack of a feature like generics justify adding complexity to the language. diff --git a/course/15-generics/exercises/3-constraints/code.go b/course/15-generics/exercises/3-constraints/code.go new file mode 100644 index 0000000..1e133cf --- /dev/null +++ b/course/15-generics/exercises/3-constraints/code.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "time" +) + +func chargeForLineItem[T lineItem](newItem T, oldItems []T, balance float64) ([]T, float64, error) { + // ? +} + +// don't edit below this line + +type lineItem interface { + GetCost() float64 + GetName() string +} + +type subscription struct { + userEmail string + startDate time.Time + interval string +} + +func (s subscription) GetName() string { + return fmt.Sprintf("%s subscription", s.interval) +} + +func (s subscription) GetCost() float64 { + if s.interval == "monthly" { + return 25.00 + } + if s.interval == "yearly" { + return 250.00 + } + return 0.0 +} + +type oneTimeUsagePlan struct { + userEmail string + numEmailsAllowed int +} + +func (otup oneTimeUsagePlan) GetName() string { + return fmt.Sprintf("one time usage plan with %v emails", otup.numEmailsAllowed) +} + +func (otup oneTimeUsagePlan) GetCost() float64 { + const costPerEmail = 0.03 + return float64(otup.numEmailsAllowed) * costPerEmail +} + +func main() { + test(subscription{ + userEmail: "john@example.com", + startDate: time.Now().UTC(), + interval: "yearly", + }, + []subscription{}, + 1000.00, + ) + test(subscription{ + userEmail: "jane@example.com", + startDate: time.Now().UTC(), + interval: "monthly", + }, + []subscription{ + { + userEmail: "jane@example.com", + startDate: time.Now().UTC().Add(-time.Hour * 24 * 7), + interval: "monthly", + }, + { + userEmail: "jane@example.com", + startDate: time.Now().UTC().Add(-time.Hour * 24 * 7 * 52 * 2), + interval: "yearly", + }, + }, + 686.20, + ) + test(oneTimeUsagePlan{ + userEmail: "dillon@example.com", + numEmailsAllowed: 5000, + }, + []oneTimeUsagePlan{}, + 756.20, + ) + test(oneTimeUsagePlan{ + userEmail: "dalton@example.com", + numEmailsAllowed: 100000, + }, + []oneTimeUsagePlan{ + { + userEmail: "dalton@example.com", + numEmailsAllowed: 34200, + }, + }, + 32.20, + ) +} + +func test[T lineItem](newItem T, oldItems []T, balance float64) { + fmt.Println(" --- ") + fmt.Printf("Charging customer for a '%s', current balance is %v...\n", newItem.GetName(), balance) + newItems, newBalance, err := chargeForLineItem(newItem, oldItems, balance) + if err != nil { + fmt.Printf("Got error: %v\n", err) + return + } + fmt.Printf("New balance is: %v. Total number of line items is now %v\n", newBalance, len(newItems)) +} diff --git a/course/15-generics/exercises/3-constraints/complete.go b/course/15-generics/exercises/3-constraints/complete.go new file mode 100644 index 0000000..7fc2570 --- /dev/null +++ b/course/15-generics/exercises/3-constraints/complete.go @@ -0,0 +1,117 @@ +package main + +import ( + "errors" + "fmt" + "time" +) + +func chargeForLineItem[T lineItem](newItem T, oldItems []T, balance float64) ([]T, float64, error) { + newBalance := balance - newItem.GetCost() + if newBalance < 0 { + return nil, 0, errors.New("insufficient funds") + } + oldItems = append(oldItems, newItem) + return oldItems, newBalance, nil +} + +// don't edit below this line + +type lineItem interface { + GetCost() float64 + GetName() string +} + +type subscription struct { + userEmail string + startDate time.Time + interval string +} + +func (s subscription) GetName() string { + return fmt.Sprintf("%s subscription", s.interval) +} + +func (s subscription) GetCost() float64 { + if s.interval == "monthly" { + return 25.00 + } + if s.interval == "yearly" { + return 250.00 + } + return 0.0 +} + +type oneTimeUsagePlan struct { + userEmail string + numEmailsAllowed int +} + +func (otup oneTimeUsagePlan) GetName() string { + return fmt.Sprintf("one time usage plan with %v emails", otup.numEmailsAllowed) +} + +func (otup oneTimeUsagePlan) GetCost() float64 { + const costPerEmail = 0.03 + return float64(otup.numEmailsAllowed) * costPerEmail +} + +func main() { + test(subscription{ + userEmail: "john@example.com", + startDate: time.Now().UTC(), + interval: "yearly", + }, + []subscription{}, + 1000.00, + ) + test(subscription{ + userEmail: "jane@example.com", + startDate: time.Now().UTC(), + interval: "monthly", + }, + []subscription{ + { + userEmail: "jane@example.com", + startDate: time.Now().UTC().Add(-time.Hour * 24 * 7), + interval: "monthly", + }, + { + userEmail: "jane@example.com", + startDate: time.Now().UTC().Add(-time.Hour * 24 * 7 * 52 * 2), + interval: "yearly", + }, + }, + 686.20, + ) + test(oneTimeUsagePlan{ + userEmail: "dillon@example.com", + numEmailsAllowed: 5000, + }, + []oneTimeUsagePlan{}, + 756.20, + ) + test(oneTimeUsagePlan{ + userEmail: "dalton@example.com", + numEmailsAllowed: 100000, + }, + []oneTimeUsagePlan{ + { + userEmail: "dalton@example.com", + numEmailsAllowed: 34200, + }, + }, + 32.20, + ) +} + +func test[T lineItem](newItem T, oldItems []T, balance float64) { + fmt.Println(" --- ") + fmt.Printf("Charging customer for a '%s', current balance is %v...\n", newItem.GetName(), balance) + newItems, newBalance, err := chargeForLineItem(newItem, oldItems, balance) + if err != nil { + fmt.Printf("Got error: %v\n", err) + return + } + fmt.Printf("New balance is: %v. Total number of line items is now %v\n", newBalance, len(newItems)) +} diff --git a/course/15-generics/exercises/3-constraints/expected.txt b/course/15-generics/exercises/3-constraints/expected.txt new file mode 100644 index 0000000..1d81be4 --- /dev/null +++ b/course/15-generics/exercises/3-constraints/expected.txt @@ -0,0 +1,12 @@ + --- +Charging customer for a 'yearly subscription', current balance is 1000... +New balance is: 750. Total number of line items is now 1 + --- +Charging customer for a 'monthly subscription', current balance is 686.2... +New balance is: 661.2. Total number of line items is now 3 + --- +Charging customer for a 'one time usage plan with 5000 emails', current balance is 756.2... +New balance is: 606.2. Total number of line items is now 1 + --- +Charging customer for a 'one time usage plan with 100000 emails', current balance is 32.2... +Got error: insufficient funds diff --git a/course/15-generics/exercises/3-constraints/readme.md b/course/15-generics/exercises/3-constraints/readme.md new file mode 100644 index 0000000..d4f5253 --- /dev/null +++ b/course/15-generics/exercises/3-constraints/readme.md @@ -0,0 +1,44 @@ +# Constraints + +Sometimes you need the logic in your generic function to know *something* about the types it operates on. The example we used in the first exercise didn't need to know *anything* about the types in the slice, so we used the built-in `any` constraint: + +```go +func splitAnySlice[T any](s []T) ([]T, []T) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` + +Constraints are just interfaces that allow us to write generics that only operate within the constraint of a given interface type. In the example above, the `any` constraint is the same as the empty interface because it means the type in question can be *anything*. + +## Creating a custom constraint + +Let's take a look at the example of a `concat` function. It takes a slice of values and concatenates the values into a string. This should work with *any type that can represent itself as a string*, even if it's not a string under the hood. For example, a `user` struct can have a `.String()` that returns a string with the user's name and age. + +```go +type stringer interface { + String() string +} + +func concat[T stringer](vals []T) string { + result := "" + for _, val := range vals { + // this is where the .String() method + // is used. That's why we need a more specific + // constraint instead of the any constraint + result += val.String() + } + return result +} +``` + +## Assignment + +We have different kinds of "line items" that we charge our customer's credit cards for. Line items can be things like "subscriptions" or "one-time payments" for email usage. + +Complete the `chargeForLineItem` function. First, it should check if the user has a balance with enough funds to be able to pay for the cost of the `newItem`. If they don't then return an "insufficient funds" error. + +If they *do* have enough funds: + +* Add the line item to the user's history by appending the `newItem` to the slice of `oldItems`. This new slice is your first return value. +* Calculate the user's new balance by subtracting the cost of the new item from their balance. This is your second return value. diff --git a/course/15-generics/exercises/4-interface_type_lists/multiple_choice.json b/course/15-generics/exercises/4-interface_type_lists/multiple_choice.json new file mode 100644 index 0000000..903aba4 --- /dev/null +++ b/course/15-generics/exercises/4-interface_type_lists/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Why might you create an interface using a type list?", + "answers": [ + "You know exactly which types satisfy your interface", + "It's too much trouble to define the methods required by your interface" + ] +} diff --git a/course/15-generics/exercises/4-interface_type_lists/readme.md b/course/15-generics/exercises/4-interface_type_lists/readme.md new file mode 100644 index 0000000..c7922cc --- /dev/null +++ b/course/15-generics/exercises/4-interface_type_lists/readme.md @@ -0,0 +1,16 @@ +# Interface type lists + +When generics were released, a new way of writing interfaces was also released at the same time! + +We can now simply list a bunch of types to get a new interface/constraint. + +```go +// Ordered is a type constraint that matches any ordered type. +// An ordered type is one that supports the <, <=, >, and >= operators. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} +``` diff --git a/course/15-generics/exercises/5-parametric_constraints/code.go b/course/15-generics/exercises/5-parametric_constraints/code.go new file mode 100644 index 0000000..f0fb000 --- /dev/null +++ b/course/15-generics/exercises/5-parametric_constraints/code.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" +) + +// ? + +// don't edit below this line + +type userBiller struct { + Plan string +} + +func (ub userBiller) Charge(u user) bill { + amount := 50.0 + if ub.Plan == "pro" { + amount = 100.0 + } + return bill{ + Customer: u, + Amount: amount, + } +} + +func (sb userBiller) Name() string { + return fmt.Sprintf("%s user biller", sb.Plan) +} + +type orgBiller struct { + Plan string +} + +func (ob orgBiller) Name() string { + return fmt.Sprintf("%s org biller", ob.Plan) +} + +func (ob orgBiller) Charge(o org) bill { + amount := 2000.0 + if ob.Plan == "pro" { + amount = 3000.0 + } + return bill{ + Customer: o, + Amount: amount, + } +} + +type customer interface { + GetBillingEmail() string +} + +type bill struct { + Customer customer + Amount float64 +} + +type user struct { + UserEmail string +} + +func (u user) GetBillingEmail() string { + return u.UserEmail +} + +type org struct { + Admin user + Name string +} + +func (o org) GetBillingEmail() string { + return o.Admin.GetBillingEmail() +} + +func main() { + testBiller[user]( + userBiller{Plan: "basic"}, + user{UserEmail: "joe@example.com"}, + ) + testBiller[user]( + userBiller{Plan: "basic"}, + user{UserEmail: "samuel.boggs@example.com"}, + ) + testBiller[user]( + userBiller{Plan: "pro"}, + user{UserEmail: "jade.row@example.com"}, + ) + testBiller[org]( + orgBiller{Plan: "basic"}, + org{Admin: user{UserEmail: "challis.rane@example.com"}}, + ) + testBiller[org]( + orgBiller{Plan: "pro"}, + org{Admin: user{UserEmail: "challis.rane@example.com"}}, + ) +} + +func testBiller[C customer](b biller[C], c C) { + fmt.Printf("Using '%s' to create a bill for '%s'\n", b.Name(), c.GetBillingEmail()) + bill := b.Charge(c) + fmt.Printf("Bill created for %v dollars\n", bill.Amount) + fmt.Println(" --- ") +} diff --git a/course/15-generics/exercises/5-parametric_constraints/complete.go b/course/15-generics/exercises/5-parametric_constraints/complete.go new file mode 100644 index 0000000..42f1fb3 --- /dev/null +++ b/course/15-generics/exercises/5-parametric_constraints/complete.go @@ -0,0 +1,106 @@ +package main + +import ( + "fmt" +) + +type biller[C customer] interface { + Charge(C) bill + Name() string +} + +// don't edit below this line + +type userBiller struct { + Plan string +} + +func (ub userBiller) Charge(u user) bill { + amount := 50.0 + if ub.Plan == "pro" { + amount = 100.0 + } + return bill{ + Customer: u, + Amount: amount, + } +} + +func (sb userBiller) Name() string { + return fmt.Sprintf("%s user biller", sb.Plan) +} + +type orgBiller struct { + Plan string +} + +func (ob orgBiller) Name() string { + return fmt.Sprintf("%s org biller", ob.Plan) +} + +func (ob orgBiller) Charge(o org) bill { + amount := 2000.0 + if ob.Plan == "pro" { + amount = 3000.0 + } + return bill{ + Customer: o, + Amount: amount, + } +} + +type customer interface { + GetBillingEmail() string +} + +type bill struct { + Customer customer + Amount float64 +} + +type user struct { + UserEmail string +} + +func (u user) GetBillingEmail() string { + return u.UserEmail +} + +type org struct { + Admin user + Name string +} + +func (o org) GetBillingEmail() string { + return o.Admin.GetBillingEmail() +} + +func main() { + testBiller[user]( + userBiller{Plan: "basic"}, + user{UserEmail: "joe@example.com"}, + ) + testBiller[user]( + userBiller{Plan: "basic"}, + user{UserEmail: "samuel.boggs@example.com"}, + ) + testBiller[user]( + userBiller{Plan: "pro"}, + user{UserEmail: "jade.row@example.com"}, + ) + testBiller[org]( + orgBiller{Plan: "basic"}, + org{Admin: user{UserEmail: "challis.rane@example.com"}}, + ) + testBiller[org]( + orgBiller{Plan: "pro"}, + org{Admin: user{UserEmail: "challis.rane@example.com"}}, + ) +} + +func testBiller[C customer](b biller[C], c C) { + fmt.Printf("Using '%s' to create a bill for '%s'\n", b.Name(), c.GetBillingEmail()) + bill := b.Charge(c) + fmt.Printf("Bill created for %v dollars\n", bill.Amount) + fmt.Println(" --- ") +} diff --git a/course/15-generics/exercises/5-parametric_constraints/expected.txt b/course/15-generics/exercises/5-parametric_constraints/expected.txt new file mode 100644 index 0000000..d6f2065 --- /dev/null +++ b/course/15-generics/exercises/5-parametric_constraints/expected.txt @@ -0,0 +1,15 @@ +Using 'basic user biller' to create a bill for 'joe@example.com' +Bill created for 50 dollars + --- +Using 'basic user biller' to create a bill for 'samuel.boggs@example.com' +Bill created for 50 dollars + --- +Using 'pro user biller' to create a bill for 'jade.row@example.com' +Bill created for 100 dollars + --- +Using 'basic org biller' to create a bill for 'challis.rane@example.com' +Bill created for 2000 dollars + --- +Using 'pro org biller' to create a bill for 'challis.rane@example.com' +Bill created for 3000 dollars + --- diff --git a/course/15-generics/exercises/5-parametric_constraints/readme.md b/course/15-generics/exercises/5-parametric_constraints/readme.md new file mode 100644 index 0000000..3b5e1b8 --- /dev/null +++ b/course/15-generics/exercises/5-parametric_constraints/readme.md @@ -0,0 +1,126 @@ +# Parametric Constraints + +Your interface definitions, which can later be used as constraints, can accept type parameters as well. + +```go +// The store interface represents a store that sells products. +// It takes a type parameter P that represents the type of products the store sells. +type store[P product] interface { + Sell(P) +} + +type product interface { + Price() float64 + Name() string +} + +type book struct { + title string + author string + price float64 +} + +func (b book) Price() float64 { + return b.price +} + +func (b book) Name() string { + return fmt.Sprintf("%s by %s", b.title, b.author) +} + +type toy struct { + name string + price float64 +} + +func (t toy) Price() float64 { + return t.price +} + +func (t toy) Name() string { + return t.name +} + +// The bookStore struct represents a store that sells books. +type bookStore struct { + booksSold []book +} + +// Sell adds a book to the bookStore's inventory. +func (bs *bookStore) Sell(b book) { + bs.booksSold = append(bs.booksSold, b) +} + +// The toyStore struct represents a store that sells toys. +type toyStore struct { + toysSold []toy +} + +// Sell adds a toy to the toyStore's inventory. +func (ts *toyStore) Sell(t toy) { + ts.toysSold = append(ts.toysSold, t) +} + +// sellProducts takes a store and a slice of products and sells +// each product one by one. +func sellProducts[P product](s store[P], products []P) { + for _, p := range products { + s.Sell(p) + } +} + +func main() { + bs := bookStore{ + booksSold: []book{}, + } + + // By passing in "book" as a type parameter, we can use the sellProducts function to sell books in a bookStore + sellProducts[book](&bs, []book{ + { + title: "The Hobbit", + author: "J.R.R. Tolkien", + price: 10.0, + }, + { + title: "The Lord of the Rings", + author: "J.R.R. Tolkien", + price: 20.0, + }, + }) + fmt.Println(bs.booksSold) + + // We can then do the same for toys + ts := toyStore{ + toysSold: []toy{}, + } + sellProducts[toy](&ts, []toy{ + { + name: "Lego", + price: 10.0, + }, + { + name: "Barbie", + price: 20.0, + }, + }) + fmt.Println(ts.toysSold) +} +``` + +## Assignment + +The chief architect at Mailio has decided she wants to implement billing with generics. Specifically, she wants us to create a new `biller` interface. A `biller` is an interface that can be used to charge a `customer`, and it can also report its `name`. + +There are two kinds of billers: + +* `userBiller` (cheaper) +* `orgBiller` (more expensive) + +A `customer` is either a `user` or an `org`. A `user` will be billed with a `userBiller` and an `org` with an `orgBiller`. + +Create the new `biller` interface. It should have 2 methods: + +* `Charge` +* `Name` + +The good news is that the architect already wrote the `userBiller` and `orgBiller` types for us that fulfill this new `biller` interface. Use the definitions of those types and their methods to figure out how to write the `biller` interface definition starting on line 7. diff --git a/course/15-generics/exercises/6-type_names/multiple_choice.json b/course/15-generics/exercises/6-type_names/multiple_choice.json new file mode 100644 index 0000000..417105c --- /dev/null +++ b/course/15-generics/exercises/6-type_names/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "The name of a type parameter...", + "answers": [ + "...can be anything, but 'T' is a common convention", + "...can and should be whatever you want", + "...must be 'T'" + ] +} diff --git a/course/15-generics/exercises/6-type_names/readme.md b/course/15-generics/exercises/6-type_names/readme.md new file mode 100644 index 0000000..39715e7 --- /dev/null +++ b/course/15-generics/exercises/6-type_names/readme.md @@ -0,0 +1,21 @@ +# Naming Generic Types + +Let's look at this simple example again: + +```go +func splitAnySlice[T any](s []T) ([]T, []T) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` + +Remember, `T` is just a variable name, We could have named the type parameter *anything*. `T` happens to be a fairly common convention for a type variable, similar to how `i` is a convention for index variables in loops. + +This is just as valid: + +```go +func splitAnySlice[MyAnyType any](s []MyAnyType) ([]MyAnyType, []MyAnyType) { + mid := len(s)/2 + return s[:mid], s[mid:] +} +``` diff --git a/course/16-go_facts/exercises/1-proverbs/multiple_choice.json b/course/16-go_facts/exercises/1-proverbs/multiple_choice.json new file mode 100644 index 0000000..8cc66a5 --- /dev/null +++ b/course/16-go_facts/exercises/1-proverbs/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which is better?", + "answers": [ + "Clear", + "Clever" + ] +} diff --git a/course/16-go_facts/exercises/1-proverbs/readme.md b/course/16-go_facts/exercises/1-proverbs/readme.md new file mode 100644 index 0000000..32a910e --- /dev/null +++ b/course/16-go_facts/exercises/1-proverbs/readme.md @@ -0,0 +1,43 @@ +# Go Proverbs + +Similar to the [Zen of Python](https://peps.python.org/pep-0020/) that we covered in the "Learn Python" course, the [Go Proverbs](https://go-proverbs.github.io/) are a beautiful collection of wise words from Rob Pike, one of Go's creators. + +> Don't communicate by sharing memory, share memory by communicating. +> +> Concurrency is not parallelism. +> +> Channels orchestrate; mutexes serialize. +> +> The bigger the interface, the weaker the abstraction. +> +> Make the zero value useful. +> +> interface{} says nothing. +> +> Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. +> +> A little copying is better than a little dependency. +> +> Syscall must always be guarded with build tags. +> +> Cgo must always be guarded with build tags. +> +> Cgo is not Go. +> +> With the unsafe package there are no guarantees. +> +> Clear is better than clever. +> +> Reflection is never clear. +> +> Errors are values. +> +> Don't just check errors, handle them gracefully. +> +> Design the architecture, name the components, document the details. +> +> Documentation is for users. +> +> Don't panic. + +@[youtube](https://www.youtube.com/watch?v=PAAkCSZUG1c) diff --git a/course/16-go_facts/exercises/1a-proverbs/multiple_choice.json b/course/16-go_facts/exercises/1a-proverbs/multiple_choice.json new file mode 100644 index 0000000..66f40d7 --- /dev/null +++ b/course/16-go_facts/exercises/1a-proverbs/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which is better?", + "answers": [ + "Copying a little code", + "Including a little dependency" + ] +} diff --git a/course/16-go_facts/exercises/1a-proverbs/readme.md b/course/16-go_facts/exercises/1a-proverbs/readme.md new file mode 100644 index 0000000..32a910e --- /dev/null +++ b/course/16-go_facts/exercises/1a-proverbs/readme.md @@ -0,0 +1,43 @@ +# Go Proverbs + +Similar to the [Zen of Python](https://peps.python.org/pep-0020/) that we covered in the "Learn Python" course, the [Go Proverbs](https://go-proverbs.github.io/) are a beautiful collection of wise words from Rob Pike, one of Go's creators. + +> Don't communicate by sharing memory, share memory by communicating. +> +> Concurrency is not parallelism. +> +> Channels orchestrate; mutexes serialize. +> +> The bigger the interface, the weaker the abstraction. +> +> Make the zero value useful. +> +> interface{} says nothing. +> +> Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. +> +> A little copying is better than a little dependency. +> +> Syscall must always be guarded with build tags. +> +> Cgo must always be guarded with build tags. +> +> Cgo is not Go. +> +> With the unsafe package there are no guarantees. +> +> Clear is better than clever. +> +> Reflection is never clear. +> +> Errors are values. +> +> Don't just check errors, handle them gracefully. +> +> Design the architecture, name the components, document the details. +> +> Documentation is for users. +> +> Don't panic. + +@[youtube](https://www.youtube.com/watch?v=PAAkCSZUG1c) diff --git a/course/16-go_facts/exercises/1b-proverbs/multiple_choice.json b/course/16-go_facts/exercises/1b-proverbs/multiple_choice.json new file mode 100644 index 0000000..bf6cea7 --- /dev/null +++ b/course/16-go_facts/exercises/1b-proverbs/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Documentation should primarily be written for...", + "answers": [ + "Users of your code", + "Maintainers of your code", + "Yourself" + ] +} diff --git a/course/16-go_facts/exercises/1b-proverbs/readme.md b/course/16-go_facts/exercises/1b-proverbs/readme.md new file mode 100644 index 0000000..32a910e --- /dev/null +++ b/course/16-go_facts/exercises/1b-proverbs/readme.md @@ -0,0 +1,43 @@ +# Go Proverbs + +Similar to the [Zen of Python](https://peps.python.org/pep-0020/) that we covered in the "Learn Python" course, the [Go Proverbs](https://go-proverbs.github.io/) are a beautiful collection of wise words from Rob Pike, one of Go's creators. + +> Don't communicate by sharing memory, share memory by communicating. +> +> Concurrency is not parallelism. +> +> Channels orchestrate; mutexes serialize. +> +> The bigger the interface, the weaker the abstraction. +> +> Make the zero value useful. +> +> interface{} says nothing. +> +> Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. +> +> A little copying is better than a little dependency. +> +> Syscall must always be guarded with build tags. +> +> Cgo must always be guarded with build tags. +> +> Cgo is not Go. +> +> With the unsafe package there are no guarantees. +> +> Clear is better than clever. +> +> Reflection is never clear. +> +> Errors are values. +> +> Don't just check errors, handle them gracefully. +> +> Design the architecture, name the components, document the details. +> +> Documentation is for users. +> +> Don't panic. + +@[youtube](https://www.youtube.com/watch?v=PAAkCSZUG1c) diff --git a/course/16-go_facts/exercises/1c-proverbs/multiple_choice.json b/course/16-go_facts/exercises/1c-proverbs/multiple_choice.json new file mode 100644 index 0000000..4dcb651 --- /dev/null +++ b/course/16-go_facts/exercises/1c-proverbs/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Errors are...", + "answers": [ + "Just values", + "Special", + "Exceptions", + "Something to be caught" + ] +} diff --git a/course/16-go_facts/exercises/1c-proverbs/readme.md b/course/16-go_facts/exercises/1c-proverbs/readme.md new file mode 100644 index 0000000..32a910e --- /dev/null +++ b/course/16-go_facts/exercises/1c-proverbs/readme.md @@ -0,0 +1,43 @@ +# Go Proverbs + +Similar to the [Zen of Python](https://peps.python.org/pep-0020/) that we covered in the "Learn Python" course, the [Go Proverbs](https://go-proverbs.github.io/) are a beautiful collection of wise words from Rob Pike, one of Go's creators. + +> Don't communicate by sharing memory, share memory by communicating. +> +> Concurrency is not parallelism. +> +> Channels orchestrate; mutexes serialize. +> +> The bigger the interface, the weaker the abstraction. +> +> Make the zero value useful. +> +> interface{} says nothing. +> +> Gofmt's style is no one's favorite, yet gofmt is everyone's favorite. +> +> A little copying is better than a little dependency. +> +> Syscall must always be guarded with build tags. +> +> Cgo must always be guarded with build tags. +> +> Cgo is not Go. +> +> With the unsafe package there are no guarantees. +> +> Clear is better than clever. +> +> Reflection is never clear. +> +> Errors are values. +> +> Don't just check errors, handle them gracefully. +> +> Design the architecture, name the components, document the details. +> +> Documentation is for users. +> +> Don't panic. + +@[youtube](https://www.youtube.com/watch?v=PAAkCSZUG1c) diff --git a/course/2-variables/exercises/1-basic_types/code.go b/course/2-variables/exercises/1-basic_types/code.go new file mode 100644 index 0000000..58db471 --- /dev/null +++ b/course/2-variables/exercises/1-basic_types/code.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // initialize variables here + + fmt.Printf("%v %f %v %q\n", smsSendingLimit, costPerSMS, hasPermission, username) +} diff --git a/course/2-variables/exercises/1-basic_types/complete.go b/course/2-variables/exercises/1-basic_types/complete.go new file mode 100644 index 0000000..60b0d11 --- /dev/null +++ b/course/2-variables/exercises/1-basic_types/complete.go @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func main() { + var smsSendingLimit int + var costPerSMS float64 + var hasPermission bool + var username string + fmt.Printf("%v %f %v %q\n", smsSendingLimit, costPerSMS, hasPermission, username) +} diff --git a/course/2-variables/exercises/1-basic_types/expected.txt b/course/2-variables/exercises/1-basic_types/expected.txt new file mode 100644 index 0000000..8bc1de0 --- /dev/null +++ b/course/2-variables/exercises/1-basic_types/expected.txt @@ -0,0 +1 @@ +0 0.000000 false "" diff --git a/course/2-variables/exercises/1-basic_types/readme.md b/course/2-variables/exercises/1-basic_types/readme.md new file mode 100644 index 0000000..b379678 --- /dev/null +++ b/course/2-variables/exercises/1-basic_types/readme.md @@ -0,0 +1,43 @@ +# Basic Types + +Go's basic variable types are: + +```go +bool + +string + +int int8 int16 int32 int64 +uint uint8 uint16 uint32 uint64 uintptr + +byte // alias for uint8 + +rune // alias for int32 + // represents a Unicode code point + +float32 float64 + +complex64 complex128 +``` + +We talked about `string`s and `int`s previously, and those two types should be fairly self-explanatory. A `bool` is a boolean variable, meaning it has a value of `true` or `false`. The [floating point](https://en.wikipedia.org/wiki/Floating-point_arithmetic) types (`float32` and `float64`) are used for numbers that are not integers -- that is, they have digits to the right of the decimal place, such as `3.14159`. The `float32` type uses 32 bits of precision, while the `float64` type uses 64 bits to be able to more precisely store more digits. Don't worry too much about the intricacies of the other types for now. We will cover some of them in more detail as the course progresses. + +# Declaring a variable + +Variables are declared using the `var` keyword. For example, to declare a variable called `number` of type `int`, you would write: + +```go +var number int +``` + +To declare a variable called `pi` to be of type `float64` with a value of `3.14159`, you would write: + +```go +var pi float64 = 3.14159 +``` + +The value of an initialized variable with no assignment will be its [zero value](https://tour.golang.org/basics/12). + +## Initialize some variables + +Initialize the given variables to `int`, `float64`, `bool` and `string` respectively, with their zero values. diff --git a/course/2-variables/exercises/10-conditionals/code.go b/course/2-variables/exercises/10-conditionals/code.go new file mode 100644 index 0000000..1d65a7c --- /dev/null +++ b/course/2-variables/exercises/10-conditionals/code.go @@ -0,0 +1,17 @@ +package main + +import "fmt" + +func main() { + messageLen := 10 + maxMessageLen := 20 + fmt.Println("Trying to send a message of length:", messageLen, "and a max length of:", maxMessageLen) + + // don't touch above this line + + if messageLen > maxMessageLen { + fmt.Println("Message sent") + } else { + fmt.Println("Message not sent") + } +} diff --git a/course/2-variables/exercises/10-conditionals/complete.go b/course/2-variables/exercises/10-conditionals/complete.go new file mode 100644 index 0000000..5574127 --- /dev/null +++ b/course/2-variables/exercises/10-conditionals/complete.go @@ -0,0 +1,17 @@ +package main + +import "fmt" + +func main() { + messageLen := 10 + maxMessageLen := 20 + fmt.Println("Trying to send a message of length:", messageLen, "and a max length of:", maxMessageLen) + + // don't touch above this line + + if messageLen <= maxMessageLen { + fmt.Println("Message sent") + } else { + fmt.Println("Message not sent") + } +} diff --git a/course/2-variables/exercises/10-conditionals/expected.txt b/course/2-variables/exercises/10-conditionals/expected.txt new file mode 100644 index 0000000..90c2be9 --- /dev/null +++ b/course/2-variables/exercises/10-conditionals/expected.txt @@ -0,0 +1,2 @@ +Trying to send a message of length: 10 and a max length of: 20 +Message sent diff --git a/course/2-variables/exercises/10-conditionals/readme.md b/course/2-variables/exercises/10-conditionals/readme.md new file mode 100644 index 0000000..cef346c --- /dev/null +++ b/course/2-variables/exercises/10-conditionals/readme.md @@ -0,0 +1,36 @@ +# Conditionals + +`if` statements in Go don't use parentheses around the condition: + +```go +if height > 4 { + fmt.Println("You are tall enough!") +} +``` + +`else if` and `else` are supported as you would expect: + +```go +if height > 6 { + fmt.Println("You are super tall!") +} else if height > 4 { + fmt.Println("You are tall enough!") +} else { + fmt.Println("You are not tall enough!") +} +``` + +## Assignment + +Fix the bug on line `12`. The `if` statement should print "Message sent" if the `messageLen` is *less than or equal to* the `maxMessageLen`, or "Message not sent" otherwise. + +### Tips + +Here are some of the comparison operators in Go: + +* `==` equal to +* `!=` not equal to +* `<` less than +* `>` greater than +* `<=` less than or equal to +* `>=` greater than or equal to diff --git a/course/2-variables/exercises/11-if_init/multiple_choice.json b/course/2-variables/exercises/11-if_init/multiple_choice.json new file mode 100644 index 0000000..9a24ccd --- /dev/null +++ b/course/2-variables/exercises/11-if_init/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Why would you use the 'initial' section of an `if` statement?", + "answers": [ + "To keep the code concise and the scope limited", + "To speed up my code", + "To confuse other programmers" + ] +} diff --git a/course/2-variables/exercises/11-if_init/readme.md b/course/2-variables/exercises/11-if_init/readme.md new file mode 100644 index 0000000..1d2b2d1 --- /dev/null +++ b/course/2-variables/exercises/11-if_init/readme.md @@ -0,0 +1,29 @@ +# The initial statement of an if block + +An `if` conditional can have an "initial" statement. The variable(s) created in the initial statement are *only* defined within the scope of the `if` body. + +```go +if INITIAL_STATEMENT; CONDITION { +} +``` + +## Why would I use this? + +This is just some syntactic sugar that Go offers to shorten up code in some cases. For example, instead of writing: + +```go +length := getLength(email) +if length < 1 { + fmt.Println("Email is invalid") +} +``` + +We can do: + +```go +if length := getLength(email); length < 1 { + fmt.Println("Email is invalid") +} +``` + +Not only is this code a bit shorter, but it also removes `length` from the parent scope, which is convenient because we don't need it there - we only need access to it while checking a condition. diff --git a/course/2-variables/exercises/2-short_declarations/code.go b/course/2-variables/exercises/2-short_declarations/code.go new file mode 100644 index 0000000..0c47ab7 --- /dev/null +++ b/course/2-variables/exercises/2-short_declarations/code.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // declare here + + fmt.Println(congrats) +} diff --git a/course/2-variables/exercises/2-short_declarations/complete.go b/course/2-variables/exercises/2-short_declarations/complete.go new file mode 100644 index 0000000..8fa1f13 --- /dev/null +++ b/course/2-variables/exercises/2-short_declarations/complete.go @@ -0,0 +1,10 @@ +package main + +import "fmt" + +func main() { + // declare here + congrats := "happy birthday!" + + fmt.Println(congrats) +} diff --git a/course/2-variables/exercises/2-short_declarations/expected.txt b/course/2-variables/exercises/2-short_declarations/expected.txt new file mode 100644 index 0000000..07bd318 --- /dev/null +++ b/course/2-variables/exercises/2-short_declarations/expected.txt @@ -0,0 +1 @@ +happy birthday! diff --git a/course/2-variables/exercises/2-short_declarations/readme.md b/course/2-variables/exercises/2-short_declarations/readme.md new file mode 100644 index 0000000..1773299 --- /dev/null +++ b/course/2-variables/exercises/2-short_declarations/readme.md @@ -0,0 +1,30 @@ +# Short Variable Declaration + +Inside a function (even the main function), the `:=` short assignment statement can be used in place of a `var` declaration. The `:=` operator infers the type of the new variable based on the value. + +```go +var empty string +``` + +Is the same as + + +```go +empty := "" +``` + +```go +numCars := 10 // inferred to be an integer + +temperature := 0.0 // temperature is inferred to be a floating point value because it has a decimal point + +var isFunny = true // isFunny is inferred to be a boolean +``` + +Outside of a function (in the [global/package scope](https://dave.cheney.net/2017/06/11/go-without-package-scoped-variables)), every statement begins with a keyword (`var`, `func`, and so on) and so the `:=` construct is not available. + +## Assignment + +A lot of our users send birthday messages using the Textio API. + +Declare a variable named `congrats` with the value "happy birthday!" using a short variable declaration. diff --git a/course/2-variables/exercises/3-type_inference/code.go b/course/2-variables/exercises/3-type_inference/code.go new file mode 100644 index 0000000..8bc373f --- /dev/null +++ b/course/2-variables/exercises/3-type_inference/code.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + penniesPerText := 2 + fmt.Printf("The type of penniesPerText is %T\n", penniesPerText) +} diff --git a/course/2-variables/exercises/3-type_inference/complete.go b/course/2-variables/exercises/3-type_inference/complete.go new file mode 100644 index 0000000..d70a15f --- /dev/null +++ b/course/2-variables/exercises/3-type_inference/complete.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + penniesPerText := 0.02 + fmt.Printf("The type of penniesPerText is %T\n", penniesPerText) +} diff --git a/course/2-variables/exercises/3-type_inference/expected.txt b/course/2-variables/exercises/3-type_inference/expected.txt new file mode 100644 index 0000000..e78f311 --- /dev/null +++ b/course/2-variables/exercises/3-type_inference/expected.txt @@ -0,0 +1 @@ +The type of penniesPerText is float64 diff --git a/course/2-variables/exercises/3-type_inference/readme.md b/course/2-variables/exercises/3-type_inference/readme.md new file mode 100644 index 0000000..5d5ebf7 --- /dev/null +++ b/course/2-variables/exercises/3-type_inference/readme.md @@ -0,0 +1,24 @@ +# Type Inference + +To declare a variable without specifying an explicit type (either by using the `:=` syntax or `var = expression` syntax), the variable's type is *inferred* from the value on the right hand side. + +When the right hand side of the declaration is typed, the new variable is of that same type: + +```go +var i int +j := i // j is also an int +``` + +However, when the right hand side is a literal value (an untyped numeric constant like `42` or `3.14`), the new variable will be an `int`, `float64`, or `complex128` depending on its precision: + +```go +i := 42 // int +f := 3.14 // float64 +g := 0.867 + 0.5i // complex128 +``` + +## Assignment + +Our current price to send a text message is 2 cents. However, it's likely that in the future the price will be a fraction of a penny, so we should use a `float64` to store this value. + +Edit the `penniesPerText` declaration so that it's inferred by the compiler to be a `float64`. diff --git a/course/2-variables/exercises/4-same_line_declarations/code.go b/course/2-variables/exercises/4-same_line_declarations/code.go new file mode 100644 index 0000000..be737b2 --- /dev/null +++ b/course/2-variables/exercises/4-same_line_declarations/code.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // declare here + + fmt.Println(averageOpenRate, displayMessage) +} diff --git a/course/2-variables/exercises/4-same_line_declarations/complete.go b/course/2-variables/exercises/4-same_line_declarations/complete.go new file mode 100644 index 0000000..0e4c3ea --- /dev/null +++ b/course/2-variables/exercises/4-same_line_declarations/complete.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + // declare here + averageOpenRate, displayMessage := .23, "is the average open rate of your messages" + fmt.Println(averageOpenRate, displayMessage) +} diff --git a/course/2-variables/exercises/4-same_line_declarations/expected.txt b/course/2-variables/exercises/4-same_line_declarations/expected.txt new file mode 100644 index 0000000..2f9fcca --- /dev/null +++ b/course/2-variables/exercises/4-same_line_declarations/expected.txt @@ -0,0 +1 @@ +0.23 is the average open rate of your messages diff --git a/course/2-variables/exercises/4-same_line_declarations/readme.md b/course/2-variables/exercises/4-same_line_declarations/readme.md new file mode 100644 index 0000000..92c8649 --- /dev/null +++ b/course/2-variables/exercises/4-same_line_declarations/readme.md @@ -0,0 +1,17 @@ +# Same Line Declarations + +We are able to declare multiple variables on the same line: + +```go +mileage, company := 80276, "Tesla" + +// is the same as + +mileage := 80276 +company := "Tesla" +``` +## Assignment + +Within the main function, declare a float called `averageOpenRate` and string called `displayMessage` on the same line. + +Initialize them to values of `.23` and `is the average open rate of your messages` respectively before they are printed. diff --git a/course/2-variables/exercises/5-type_sizes/code.go b/course/2-variables/exercises/5-type_sizes/code.go new file mode 100644 index 0000000..b4b96d9 --- /dev/null +++ b/course/2-variables/exercises/5-type_sizes/code.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + accountAge := 2.6 + + // create a new "accountAgeInt" here + // it should be the result of casting "accountAge" to an integer + + fmt.Println("Your account has existed for", accountAgeInt, "years") +} diff --git a/course/2-variables/exercises/5-type_sizes/complete.go b/course/2-variables/exercises/5-type_sizes/complete.go new file mode 100644 index 0000000..3f4d083 --- /dev/null +++ b/course/2-variables/exercises/5-type_sizes/complete.go @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func main() { + accountAge := 2.6 + + // create a new "accountAgeInt" here + // it should be the result of casting "accountAge" to an integer + accountAgeInt := int(accountAge) + + fmt.Println("Your account has existed for", accountAgeInt, "years") +} diff --git a/course/2-variables/exercises/5-type_sizes/expected.txt b/course/2-variables/exercises/5-type_sizes/expected.txt new file mode 100644 index 0000000..1d78c2d --- /dev/null +++ b/course/2-variables/exercises/5-type_sizes/expected.txt @@ -0,0 +1 @@ +Your account has existed for 2 years diff --git a/course/2-variables/exercises/5-type_sizes/readme.md b/course/2-variables/exercises/5-type_sizes/readme.md new file mode 100644 index 0000000..05b8ec3 --- /dev/null +++ b/course/2-variables/exercises/5-type_sizes/readme.md @@ -0,0 +1,37 @@ +# Type Sizes + +Ints, [uints](https://www.cs.utah.edu/~germain/PPS/Topics/unsigned_integer.html#:~:text=Unsigned%20Integers,negative%20(zero%20or%20positive).), [floats](https://techterms.com/definition/floatingpoint), and [complex](https://www.cloudhadoop.com/2018/12/golang-tutorials-complex-types-numbers.html#:~:text=Golang%20Complex%20Type%20Numbers,complex%20number%20is%2012.8i.) numbers all have type sizes. + +```go +int int8 int16 int32 int64 // whole numbers + +uint uint8 uint16 uint32 uint64 uintptr // positive whole numbers + +float32 float64 // decimal numbers + +complex64 complex128 // imaginary numbers (rare) +``` + +The size (8, 16, 32, 64, 128, etc) indicates how many bits in memory will be used to store the variable. The default `int` and `uint` types are just aliases that refer to their respective 32 or 64 bit sizes depending on the environment of the user. + +The standard sizes that should be used unless the developer has a specific need are: + +* `int` +* `uint` +* `float64` +* `complex128` + +Some types can be converted the following way: + +```go +temperatureInt := 88 +temperatureFloat := float64(temperatureInt) +``` + +Casting a float to an integer in this way [truncates](https://techterms.com/definition/truncate) the floating point portion. + +## Assignment + +Our Textio customers want to know how long they have had accounts with us. + +Follow the instructions in the comment provided. You will create a new variable called `accountAgeInt` that will be the truncated, integer version of `accountAge`. diff --git a/course/2-variables/exercises/6-which_type_to_use/multiple_choice.json b/course/2-variables/exercises/6-which_type_to_use/multiple_choice.json new file mode 100644 index 0000000..e35a032 --- /dev/null +++ b/course/2-variables/exercises/6-which_type_to_use/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "When should you elect to NOT use a 'default type'?", + "answers": [ + "When performance and memory are the primary concern", + "When my system has lots of extra hardware I want to utilize", + "When either a 'default' or a specific size will work" + ] +} diff --git a/course/2-variables/exercises/6-which_type_to_use/readme.md b/course/2-variables/exercises/6-which_type_to_use/readme.md new file mode 100644 index 0000000..c3f9c25 --- /dev/null +++ b/course/2-variables/exercises/6-which_type_to_use/readme.md @@ -0,0 +1,28 @@ +# Which Type Should I Use? + +With so many types for what is essentially just a number, developers coming from languages that only have one kind of `Number` type (like JavaScript) may find the choices daunting. + +## Prefer "default" types + +A problem arises when we have a `uint16`, and the function we are trying to pass it into takes an `int`. We're forced to write code riddled with type casts like `int(myUint16)`. + +This style of development can be slow and annoying to read. When Go developers stray from the “default” type for any given type family, the code can get messy quickly. + +Unless you have a good reason to, stick to the following types: + +* `bool` +* `string` +* `int` +* `uint32` +* `byte` +* `rune` +* `float64` +* `complex128` + +## When should I use a more specific type? + +When you're super concerned about performance and memory usage. + +That’s about it. The only reason to deviate from the defaults is to squeeze out every last bit of performance when you are writing an application that is resource-constrained. (Or, in the special case of `uint64`, you need an absurd range of unsigned integers). + +You can [read more on this subject here](https://blog.boot.dev/golang/default-native-types-golang/) if you'd like, but it's not required. diff --git a/course/2-variables/exercises/6a-which_type_to_use/multiple_choice.json b/course/2-variables/exercises/6a-which_type_to_use/multiple_choice.json new file mode 100644 index 0000000..91e518b --- /dev/null +++ b/course/2-variables/exercises/6a-which_type_to_use/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What does the size of a type indicate?", + "answers": [ + "Bits", + "Bytes", + "Nibbles" + ] +} diff --git a/course/2-variables/exercises/6a-which_type_to_use/readme.md b/course/2-variables/exercises/6a-which_type_to_use/readme.md new file mode 100644 index 0000000..c3f9c25 --- /dev/null +++ b/course/2-variables/exercises/6a-which_type_to_use/readme.md @@ -0,0 +1,28 @@ +# Which Type Should I Use? + +With so many types for what is essentially just a number, developers coming from languages that only have one kind of `Number` type (like JavaScript) may find the choices daunting. + +## Prefer "default" types + +A problem arises when we have a `uint16`, and the function we are trying to pass it into takes an `int`. We're forced to write code riddled with type casts like `int(myUint16)`. + +This style of development can be slow and annoying to read. When Go developers stray from the “default” type for any given type family, the code can get messy quickly. + +Unless you have a good reason to, stick to the following types: + +* `bool` +* `string` +* `int` +* `uint32` +* `byte` +* `rune` +* `float64` +* `complex128` + +## When should I use a more specific type? + +When you're super concerned about performance and memory usage. + +That’s about it. The only reason to deviate from the defaults is to squeeze out every last bit of performance when you are writing an application that is resource-constrained. (Or, in the special case of `uint64`, you need an absurd range of unsigned integers). + +You can [read more on this subject here](https://blog.boot.dev/golang/default-native-types-golang/) if you'd like, but it's not required. diff --git a/course/2-variables/exercises/7-constants/code.go b/course/2-variables/exercises/7-constants/code.go new file mode 100644 index 0000000..5a03823 --- /dev/null +++ b/course/2-variables/exercises/7-constants/code.go @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func main() { + const premiumPlanName = "Premium Plan" + premiumPlanName = "Basic Plan" + + // don't edit below this line + + fmt.Println("plan:", premiumPlanName) + fmt.Println("plan:", basicPlanName) +} diff --git a/course/2-variables/exercises/7-constants/complete.go b/course/2-variables/exercises/7-constants/complete.go new file mode 100644 index 0000000..30c93b3 --- /dev/null +++ b/course/2-variables/exercises/7-constants/complete.go @@ -0,0 +1,13 @@ +package main + +import "fmt" + +func main() { + const premiumPlanName = "Premium Plan" + const basicPlanName = "Basic Plan" + + // don't edit below this line + + fmt.Println("plan:", premiumPlanName) + fmt.Println("plan:", basicPlanName) +} diff --git a/course/2-variables/exercises/7-constants/expected.txt b/course/2-variables/exercises/7-constants/expected.txt new file mode 100644 index 0000000..3e934d0 --- /dev/null +++ b/course/2-variables/exercises/7-constants/expected.txt @@ -0,0 +1,2 @@ +plan: Premium Plan +plan: Basic Plan diff --git a/course/2-variables/exercises/7-constants/readme.md b/course/2-variables/exercises/7-constants/readme.md new file mode 100644 index 0000000..ef285d3 --- /dev/null +++ b/course/2-variables/exercises/7-constants/readme.md @@ -0,0 +1,13 @@ +# Constants + +Constants are declared like variables but use the `const` keyword. Constants can't use the `:=` short declaration syntax. + +Constants can be character, string, boolean, or numeric values. They *can not* be more complex types like slices, maps and structs, which are types we will explain later. + +As the name implies, the value of a constant can't be changed after it has been declared. + +## Use two separate constants + +Something weird is happening in this code. + +What *should* be happening is that we create 2 separate constants: `premiumPlanName` and `basicPlanName`. Right now it looks like we're trying to overwrite one of them. diff --git a/course/2-variables/exercises/8-computed_constants/code.go b/course/2-variables/exercises/8-computed_constants/code.go new file mode 100644 index 0000000..f7a00b1 --- /dev/null +++ b/course/2-variables/exercises/8-computed_constants/code.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + const secondsInMinute = 60 + const minutesInHour = 60 + const secondsInHour = // ? + + // don't edit below this line + fmt.Println("number of seconds in an hour:", secondsInHour) +} diff --git a/course/2-variables/exercises/8-computed_constants/complete.go b/course/2-variables/exercises/8-computed_constants/complete.go new file mode 100644 index 0000000..994ce26 --- /dev/null +++ b/course/2-variables/exercises/8-computed_constants/complete.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + const secondsInMinute = 60 + const minutesInHour = 60 + const secondsInHour = secondsInMinute * minutesInHour + + // don't edit below this line + fmt.Println("number of seconds in an hour:", secondsInHour) +} diff --git a/course/2-variables/exercises/8-computed_constants/expected.txt b/course/2-variables/exercises/8-computed_constants/expected.txt new file mode 100644 index 0000000..2ee6d2b --- /dev/null +++ b/course/2-variables/exercises/8-computed_constants/expected.txt @@ -0,0 +1 @@ +number of seconds in an hour: 3600 diff --git a/course/2-variables/exercises/8-computed_constants/readme.md b/course/2-variables/exercises/8-computed_constants/readme.md new file mode 100644 index 0000000..4c63b6a --- /dev/null +++ b/course/2-variables/exercises/8-computed_constants/readme.md @@ -0,0 +1,25 @@ +# Computed Constants + +Constants must be known at compile time. More often than not they will be declared with a static value: + +```go +const myInt = 15 +``` + +However, constants *can be computed* so long as the computation can happen at *compile time*. + +For example, this is valid: + +```go +const firstName = "Lane" +const lastName = "Wagner" +const fullName = firstName + " " + lastName +``` + +That said, you *cannot* declare a constant that can only be computed at run-time. + +## Assignment + +Keeping track of time in a message-sending application is *critical*. Imagine getting an appointment reminder an hour **after** your doctor's visit. + +Complete the code using a computed constant to print the number of seconds in an hour. diff --git a/course/2-variables/exercises/9-formatting_strings/code.go b/course/2-variables/exercises/9-formatting_strings/code.go new file mode 100644 index 0000000..23ad45b --- /dev/null +++ b/course/2-variables/exercises/9-formatting_strings/code.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + const name = "Saul Goodman" + const openRate = 30.5 + + // ? + + // don't edit below this line + + fmt.Println(msg) +} diff --git a/course/2-variables/exercises/9-formatting_strings/complete.go b/course/2-variables/exercises/9-formatting_strings/complete.go new file mode 100644 index 0000000..ce3c13e --- /dev/null +++ b/course/2-variables/exercises/9-formatting_strings/complete.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + const name = "Saul Goodman" + const openRate = 30.5 + + msg := fmt.Sprintf("Hi %s, your open rate is %.1f percent", name, openRate) + + // don't edit below this line + + fmt.Println(msg) +} diff --git a/course/2-variables/exercises/9-formatting_strings/expected.txt b/course/2-variables/exercises/9-formatting_strings/expected.txt new file mode 100644 index 0000000..2070df7 --- /dev/null +++ b/course/2-variables/exercises/9-formatting_strings/expected.txt @@ -0,0 +1 @@ +Hi Saul Goodman, your open rate is 30.5 percent diff --git a/course/2-variables/exercises/9-formatting_strings/readme.md b/course/2-variables/exercises/9-formatting_strings/readme.md new file mode 100644 index 0000000..b2a67ed --- /dev/null +++ b/course/2-variables/exercises/9-formatting_strings/readme.md @@ -0,0 +1,57 @@ +# Formatting Strings in Go + +Go follows the [printf tradition](https://cplusplus.com/reference/cstdio/printf/) from the C language. In my opinion, string formatting/interpolation in Go is currently *less* elegant than JavaScript and Python. + +* [fmt.Printf](https://pkg.go.dev/fmt#Printf) - Prints a formatted string to [standard out](https://stackoverflow.com/questions/3385201/confused-about-stdin-stdout-and-stderr). +* [fmt.Sprintf()](https://pkg.go.dev/fmt#Sprintf) - Returns the formatted string + +## Examples + +### %v - Interpolate the default representation + +The `%v` variant prints the Go syntax representation of a value. You can usually use this if you're unsure what else to use. That said, it's better to use the type-specific variant if you can. + +```go +fmt.Printf("I am %v years old", 10) +// I am 10 years old + +fmt.Printf("I am %v years old", "way too many") +// I am way too many years old +``` + +### `%s` - Interpolate a string + +```go +fmt.Printf("I am %s years old", "way too many") +// I am way too many years old +``` + +### `%d` - Interpolate an integer in decimal form + +```go +fmt.Printf("I am %d years old", 10) +// I am 10 years old +``` + +### `%f` - Interpolate a decimal + +```go +fmt.Printf("I am %f years old", 10.523) +// I am 10.523000 years old + +// The ".2" rounds the number to 2 decimal places +fmt.Printf("I am %.2f years old", 10.523) +// I am 10.53 years old +``` + +If you're interested in all the formatting options, feel free to take a look at the `fmt` package's [docs here](https://pkg.go.dev/fmt#hdr-Printing). + +## Assignment + +Create a new variable called `msg` on line 9. It's a string that contains the following: + +``` +Hi NAME, your open rate is OPENRATE percent +``` + +Where `NAME` is the given `name`, and `OPENRATE` is the `openRate` rounded to the nearest "tenths" place. diff --git a/course/3-functions/exercises/1-intro/code.go b/course/3-functions/exercises/1-intro/code.go new file mode 100644 index 0000000..afe2aea --- /dev/null +++ b/course/3-functions/exercises/1-intro/code.go @@ -0,0 +1,19 @@ +package main + +import "fmt" + +func concat(s1, s2) string { + return s1 + s2 +} + +// don't touch below this line + +func main() { + test("Lane,", " happy birthday!") + test("Elon,", " hope that Tesla thing works out") + test("Go", " is fantastic") +} + +func test(s1 string, s2 string) { + fmt.Println(concat(s1, s2)) +} diff --git a/course/3-functions/exercises/1-intro/complete.go b/course/3-functions/exercises/1-intro/complete.go new file mode 100644 index 0000000..3c8c8e9 --- /dev/null +++ b/course/3-functions/exercises/1-intro/complete.go @@ -0,0 +1,19 @@ +package main + +import "fmt" + +func concat(s1 string, s2 string) string { + return s1 + s2 +} + +// don't touch below this line + +func main() { + test("Lane,", " happy birthday!") + test("Elon,", " hope that Tesla thing works out") + test("Go", " is fantastic") +} + +func test(s1 string, s2 string) { + fmt.Println(concat(s1, s2)) +} diff --git a/course/3-functions/exercises/1-intro/expected.txt b/course/3-functions/exercises/1-intro/expected.txt new file mode 100644 index 0000000..aeebc66 --- /dev/null +++ b/course/3-functions/exercises/1-intro/expected.txt @@ -0,0 +1,3 @@ +Lane, happy birthday! +Elon, hope that Tesla thing works out +Go is fantastic diff --git a/course/3-functions/exercises/1-intro/readme.md b/course/3-functions/exercises/1-intro/readme.md new file mode 100644 index 0000000..6a30c8a --- /dev/null +++ b/course/3-functions/exercises/1-intro/readme.md @@ -0,0 +1,25 @@ +# Functions + +Functions in Go can take zero or more arguments. + +To make Go code easier to read, the variable type comes *after* the variable name. + +For example, the following function: + +```go +func sub(x int, y int) int { + return x-y +} +``` + +Accepts two integer parameters and returns another integer. + +Here, `func sub(x int, y int) int` is known as the "function signature". + +## Assignment + +We often will need to manipulate strings in our messaging app. For example, adding some personalization by using a customer's name within a template. The `concat` function should take two strings and smash them together. + +* `hello` + `world` = `helloworld` + +Fix the [function signature](https://www.devx.com/open-source-zone/programming-basics-the-function-signature/#:~:text=A%20function%20signature%20includes%20the%20function%20name%2C%20its%20arguments%2C%20and%20in%20some%20languages%2C%20the%20return%20type.) of `concat` to reflect its behavior. diff --git a/course/3-functions/exercises/2-multiple_params/multiple_choice.json b/course/3-functions/exercises/2-multiple_params/multiple_choice.json new file mode 100644 index 0000000..1b0e4ec --- /dev/null +++ b/course/3-functions/exercises/2-multiple_params/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which of the following is the most succinct way to write a function signature?", + "answers": [ + "func createUser(firstName, lastName string, age int)", + "func createUser(firstName string, lastName string, age int)" + ] +} diff --git a/course/3-functions/exercises/2-multiple_params/readme.md b/course/3-functions/exercises/2-multiple_params/readme.md new file mode 100644 index 0000000..d97419a --- /dev/null +++ b/course/3-functions/exercises/2-multiple_params/readme.md @@ -0,0 +1,13 @@ +# Multiple Parameters + +When multiple arguments are of the same type, the type only needs to be declared after the last one, assuming they are in order. + +For example: + +```go +func add(x, y int) int { + return x + y +} +``` + +If they are not in order they need to be defined separately. diff --git a/course/3-functions/exercises/3-declaration_syntax/multiple_choice.json b/course/3-functions/exercises/3-declaration_syntax/multiple_choice.json new file mode 100644 index 0000000..45208ee --- /dev/null +++ b/course/3-functions/exercises/3-declaration_syntax/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What are we talking about when we discuss 'declaration syntax'?", + "answers": [ + "The style of language used to create new variables, types, functions, etc...", + "The decision about whether to use camelCase or snake_case", + "The argument over guard clauses vs if-else statements", + "The ever-important question of tabs vs spaces" + ] +} diff --git a/course/3-functions/exercises/3-declaration_syntax/readme.md b/course/3-functions/exercises/3-declaration_syntax/readme.md new file mode 100644 index 0000000..544929d --- /dev/null +++ b/course/3-functions/exercises/3-declaration_syntax/readme.md @@ -0,0 +1,39 @@ +# Declaration Syntax + +Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages. + +## C-Style syntax + +The C language describes types with an expression including the name to be declared, and states what type that expression will have. + +```c +int y; +``` + +The code above declares `y` as an `int`. In general, the type goes on the left and the expression on the right. + +Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare. + +```c +int (*fp)(int (*ff)(int x, int y), int b) +``` + +## Go-style syntax + +Go's declarations are clear, you just read them left to right, just like you would in English. + +```go +x int +p *int +a [3]int +``` + +It's nice for more complex signatures, it makes them easier to read. + +```go +f func(func(int,int) int, int) int +``` + +## Reference + +The [following post on the Go blog](https://blog.golang.org/declaration-syntax) is a great resource for further reading on declaration syntax. diff --git a/course/3-functions/exercises/3a-declaration_syntax/multiple_choice.json b/course/3-functions/exercises/3a-declaration_syntax/multiple_choice.json new file mode 100644 index 0000000..f4d3743 --- /dev/null +++ b/course/3-functions/exercises/3a-declaration_syntax/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which language's declaration syntax reads like English from left-to-right", + "answers": [ + "Go", + "C" + ] +} diff --git a/course/3-functions/exercises/3a-declaration_syntax/readme.md b/course/3-functions/exercises/3a-declaration_syntax/readme.md new file mode 100644 index 0000000..544929d --- /dev/null +++ b/course/3-functions/exercises/3a-declaration_syntax/readme.md @@ -0,0 +1,39 @@ +# Declaration Syntax + +Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages. + +## C-Style syntax + +The C language describes types with an expression including the name to be declared, and states what type that expression will have. + +```c +int y; +``` + +The code above declares `y` as an `int`. In general, the type goes on the left and the expression on the right. + +Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare. + +```c +int (*fp)(int (*ff)(int x, int y), int b) +``` + +## Go-style syntax + +Go's declarations are clear, you just read them left to right, just like you would in English. + +```go +x int +p *int +a [3]int +``` + +It's nice for more complex signatures, it makes them easier to read. + +```go +f func(func(int,int) int, int) int +``` + +## Reference + +The [following post on the Go blog](https://blog.golang.org/declaration-syntax) is a great resource for further reading on declaration syntax. diff --git a/course/3-functions/exercises/3b-declaration_syntax/multiple_choice.json b/course/3-functions/exercises/3b-declaration_syntax/multiple_choice.json new file mode 100644 index 0000000..0b19418 --- /dev/null +++ b/course/3-functions/exercises/3b-declaration_syntax/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What is 'f func(func(int,int) int, int) int'?", + "answers": [ + "A function named 'f' that takes a function and an int as arguments and returns an int", + "A function named 'f' that takes an int as the argument and returns an int", + "A function named 'f' that takes a function as the argument and returns an int", + "A function named 'f' that takes a function and an int as arguments and returns a function" + ] +} diff --git a/course/3-functions/exercises/3b-declaration_syntax/readme.md b/course/3-functions/exercises/3b-declaration_syntax/readme.md new file mode 100644 index 0000000..544929d --- /dev/null +++ b/course/3-functions/exercises/3b-declaration_syntax/readme.md @@ -0,0 +1,39 @@ +# Declaration Syntax + +Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages. + +## C-Style syntax + +The C language describes types with an expression including the name to be declared, and states what type that expression will have. + +```c +int y; +``` + +The code above declares `y` as an `int`. In general, the type goes on the left and the expression on the right. + +Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare. + +```c +int (*fp)(int (*ff)(int x, int y), int b) +``` + +## Go-style syntax + +Go's declarations are clear, you just read them left to right, just like you would in English. + +```go +x int +p *int +a [3]int +``` + +It's nice for more complex signatures, it makes them easier to read. + +```go +f func(func(int,int) int, int) int +``` + +## Reference + +The [following post on the Go blog](https://blog.golang.org/declaration-syntax) is a great resource for further reading on declaration syntax. diff --git a/course/3-functions/exercises/4-pass_by_value/code.go b/course/3-functions/exercises/4-pass_by_value/code.go new file mode 100644 index 0000000..46eaee4 --- /dev/null +++ b/course/3-functions/exercises/4-pass_by_value/code.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + sendsSoFar := 430 + const sendsToAdd = 25 + incrementSends(sendsSoFar, sendsToAdd) + fmt.Println("you've sent", sendsSoFar, "messages") +} + +func incrementSends(sendsSoFar, sendsToAdd int) int { + sendsSoFar = sendsSoFar + sendsToAdd +} diff --git a/course/3-functions/exercises/4-pass_by_value/complete.go b/course/3-functions/exercises/4-pass_by_value/complete.go new file mode 100644 index 0000000..c6b5c98 --- /dev/null +++ b/course/3-functions/exercises/4-pass_by_value/complete.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + sendsSoFar := 430 + const sendsToAdd = 25 + sendsSoFar = incrementSends(sendsSoFar, sendsToAdd) + fmt.Println("you've sent", sendsSoFar, "messages") +} + +func incrementSends(sendsSoFar, sendsToAdd int) int { + return sendsSoFar + sendsToAdd +} diff --git a/course/3-functions/exercises/4-pass_by_value/expected.txt b/course/3-functions/exercises/4-pass_by_value/expected.txt new file mode 100644 index 0000000..8632997 --- /dev/null +++ b/course/3-functions/exercises/4-pass_by_value/expected.txt @@ -0,0 +1 @@ +you've sent 455 messages diff --git a/course/3-functions/exercises/4-pass_by_value/readme.md b/course/3-functions/exercises/4-pass_by_value/readme.md new file mode 100644 index 0000000..4c72b86 --- /dev/null +++ b/course/3-functions/exercises/4-pass_by_value/readme.md @@ -0,0 +1,26 @@ +# Passing Variables by Value + +Variables in Go are passed by value (except for a few data types we haven't covered yet). "Pass by value" means that when a variable is passed into a function, that function receives a *copy* of the variable. The function is unable to mutate the caller's data. + +```go +func main(){ + x := 5 + increment(x) + + fmt.Println(x) + // still prints 5, + // because the increment function + // received a copy of x +} + +func increment(x int){ + x++ +} +``` + +## Assignment + +It's critical in Textio that we keep track of how many SMS messages we send on behalf of our clients. Fix the bug to accurately track the number of SMS messages sent. + +1. Alter the `incrementSends` function so that it returns the result after incrementing the `sendsSoFar`. +2. Alter `main()` to capture the return value from `incrementSends()` and overwrite the previous `sendsSoFar` value. diff --git a/course/3-functions/exercises/5-ignoring_return_values/code.go b/course/3-functions/exercises/5-ignoring_return_values/code.go new file mode 100644 index 0000000..c0e5b0e --- /dev/null +++ b/course/3-functions/exercises/5-ignoring_return_values/code.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + firstName, lastName := getNames() + fmt.Println("Welcome to Textio,", firstName) +} + +// don't edit below this line + +func getNames() (string, string) { + return "John", "Doe" +} diff --git a/course/3-functions/exercises/5-ignoring_return_values/complete.go b/course/3-functions/exercises/5-ignoring_return_values/complete.go new file mode 100644 index 0000000..4de9048 --- /dev/null +++ b/course/3-functions/exercises/5-ignoring_return_values/complete.go @@ -0,0 +1,14 @@ +package main + +import "fmt" + +func main() { + firstName, _ := getNames() + fmt.Println("Welcome to Textio,", firstName) +} + +// don't edit below this line + +func getNames() (string, string) { + return "John", "Doe" +} diff --git a/course/3-functions/exercises/5-ignoring_return_values/expected.txt b/course/3-functions/exercises/5-ignoring_return_values/expected.txt new file mode 100644 index 0000000..1028348 --- /dev/null +++ b/course/3-functions/exercises/5-ignoring_return_values/expected.txt @@ -0,0 +1 @@ +Welcome to Textio, John diff --git a/course/3-functions/exercises/5-ignoring_return_values/readme.md b/course/3-functions/exercises/5-ignoring_return_values/readme.md new file mode 100644 index 0000000..b9538c5 --- /dev/null +++ b/course/3-functions/exercises/5-ignoring_return_values/readme.md @@ -0,0 +1,25 @@ +# Ignoring Return Values + +A function can return a value that the caller doesn't care about. We can explicitly ignore variables by using an underscore: `_` + +For example: + +```go +func getPoint() (x int, y int) { + return 3, 4 +} + +// ignore y value +x, _ := getPoint() +``` +Even though `getPoint()` returns two values, we can capture the first one and ignore the second. + +## Why would you ignore a return value? + +There could be many reasons. For example, maybe a function called `getCircle` returns the center point and the radius, but you really only need the radius for your calculation. In that case, you would ignore the center point variable. + +This is crucial to understand because the Go compiler will throw an error if you have unused variable declarations in your code, so you *need* to ignore anything you don't intend to use. + +## Assignment + +In this code snippet, we only need our customer's first name. Ignore the last name so that the code compiles. diff --git a/course/3-functions/exercises/6-named_return_values/code.go b/course/3-functions/exercises/6-named_return_values/code.go new file mode 100644 index 0000000..98caf7b --- /dev/null +++ b/course/3-functions/exercises/6-named_return_values/code.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" +) + +func yearsUntilEvents(age int) (int, int, int) { + yearsUntilAdult = 18 - age + if yearsUntilAdult < 0 { + yearsUntilAdult = 0 + } + yearsUntilDrinking = 21 - age + if yearsUntilDrinking < 0 { + yearsUntilDrinking = 0 + } + yearsUntilCarRental = 25 - age + if yearsUntilCarRental < 0 { + yearsUntilCarRental = 0 + } + return +} + +// don't edit below this line + +func test(age int) { + fmt.Println("Age:", age) + yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental := yearsUntilEvents(age) + fmt.Println("You are an adult in", yearsUntilAdult, "years") + fmt.Println("You can drink in", yearsUntilDrinking, "years") + fmt.Println("You can rent a car in", yearsUntilCarRental, "years") + fmt.Println("====================================") +} + +func main() { + test(4) + test(10) + test(22) + test(35) +} diff --git a/course/3-functions/exercises/6-named_return_values/complete.go b/course/3-functions/exercises/6-named_return_values/complete.go new file mode 100644 index 0000000..b0bd283 --- /dev/null +++ b/course/3-functions/exercises/6-named_return_values/complete.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" +) + +func yearsUntilEvents(age int) (yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental int) { + yearsUntilAdult = 18 - age + if yearsUntilAdult < 0 { + yearsUntilAdult = 0 + } + yearsUntilDrinking = 21 - age + if yearsUntilDrinking < 0 { + yearsUntilDrinking = 0 + } + yearsUntilCarRental = 25 - age + if yearsUntilCarRental < 0 { + yearsUntilCarRental = 0 + } + return +} + +// don't edit below this line + +func test(age int) { + fmt.Println("Age:", age) + yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental := yearsUntilEvents(age) + fmt.Println("You are an adult in", yearsUntilAdult, "years") + fmt.Println("You can drink in", yearsUntilDrinking, "years") + fmt.Println("You can rent a car in", yearsUntilCarRental, "years") + fmt.Println("====================================") +} + +func main() { + test(4) + test(10) + test(22) + test(35) +} diff --git a/course/3-functions/exercises/6-named_return_values/expected.txt b/course/3-functions/exercises/6-named_return_values/expected.txt new file mode 100644 index 0000000..e0d8e91 --- /dev/null +++ b/course/3-functions/exercises/6-named_return_values/expected.txt @@ -0,0 +1,20 @@ +Age: 4 +You are an adult in 14 years +You can drink in 17 years +You can rent a car in 21 years +==================================== +Age: 10 +You are an adult in 8 years +You can drink in 11 years +You can rent a car in 15 years +==================================== +Age: 22 +You are an adult in 0 years +You can drink in 0 years +You can rent a car in 3 years +==================================== +Age: 35 +You are an adult in 0 years +You can drink in 0 years +You can rent a car in 0 years +==================================== diff --git a/course/3-functions/exercises/6-named_return_values/readme.md b/course/3-functions/exercises/6-named_return_values/readme.md new file mode 100644 index 0000000..2243214 --- /dev/null +++ b/course/3-functions/exercises/6-named_return_values/readme.md @@ -0,0 +1,35 @@ +# Named Return Values + +Return values may be given names, and if they are, then they are treated the same as if they were new variables defined at the top of the function. + +Named return values are best thought of as a way to document the purpose of the returned values. + +According to the [tour of go](https://tour.golang.org/): + +> A return statement without arguments returns the named return values. This is known as a "naked" return. Naked return statements should be used only in short functions. They can harm readability in longer functions. + +```go +func getCoords() (x, y int){ + // x and y are initialized with zero values + + return // automatically returns x and y +} +``` + +Is the same as: + +```go +func getCoords() (int, int){ + var x int + var y int + return x, y +} +``` + +In the first example, `x` and `y` are the return values. At the end of the function, we could simply write `return` to return the values of those two variables, rather that writing `return x,y`. + +## Assignment + +One of our clients likes us to send text messages reminding users of life events coming up. + +Fix the bug by using named return values in the function signature so the code will compile and run as intended. diff --git a/course/3-functions/exercises/6a-named_returns_explicit/code.go b/course/3-functions/exercises/6a-named_returns_explicit/code.go new file mode 100644 index 0000000..d0f3a46 --- /dev/null +++ b/course/3-functions/exercises/6a-named_returns_explicit/code.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" +) + +func yearsUntilEvents(age int) (yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental int) { + yearsUntilAdult = 18 - age + if yearsUntilAdult < 0 { + yearsUntilAdult = 0 + } + yearsUntilDrinking = 21 - age + if yearsUntilDrinking < 0 { + yearsUntilDrinking = 0 + } + yearsUntilCarRental = 25 - age + if yearsUntilCarRental < 0 { + yearsUntilCarRental = 0 + } + return 0, 0, 0 +} + +// don't edit below this line + +func test(age int) { + fmt.Println("Age:", age) + yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental := yearsUntilEvents(age) + fmt.Println("You are an adult in", yearsUntilAdult, "years") + fmt.Println("You can drink in", yearsUntilDrinking, "years") + fmt.Println("You can rent a car in", yearsUntilCarRental, "years") + fmt.Println("====================================") +} + +func main() { + test(4) + test(10) + test(22) + test(35) +} diff --git a/course/3-functions/exercises/6a-named_returns_explicit/complete.go b/course/3-functions/exercises/6a-named_returns_explicit/complete.go new file mode 100644 index 0000000..b0bd283 --- /dev/null +++ b/course/3-functions/exercises/6a-named_returns_explicit/complete.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" +) + +func yearsUntilEvents(age int) (yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental int) { + yearsUntilAdult = 18 - age + if yearsUntilAdult < 0 { + yearsUntilAdult = 0 + } + yearsUntilDrinking = 21 - age + if yearsUntilDrinking < 0 { + yearsUntilDrinking = 0 + } + yearsUntilCarRental = 25 - age + if yearsUntilCarRental < 0 { + yearsUntilCarRental = 0 + } + return +} + +// don't edit below this line + +func test(age int) { + fmt.Println("Age:", age) + yearsUntilAdult, yearsUntilDrinking, yearsUntilCarRental := yearsUntilEvents(age) + fmt.Println("You are an adult in", yearsUntilAdult, "years") + fmt.Println("You can drink in", yearsUntilDrinking, "years") + fmt.Println("You can rent a car in", yearsUntilCarRental, "years") + fmt.Println("====================================") +} + +func main() { + test(4) + test(10) + test(22) + test(35) +} diff --git a/course/3-functions/exercises/6a-named_returns_explicit/expected.txt b/course/3-functions/exercises/6a-named_returns_explicit/expected.txt new file mode 100644 index 0000000..e0d8e91 --- /dev/null +++ b/course/3-functions/exercises/6a-named_returns_explicit/expected.txt @@ -0,0 +1,20 @@ +Age: 4 +You are an adult in 14 years +You can drink in 17 years +You can rent a car in 21 years +==================================== +Age: 10 +You are an adult in 8 years +You can drink in 11 years +You can rent a car in 15 years +==================================== +Age: 22 +You are an adult in 0 years +You can drink in 0 years +You can rent a car in 3 years +==================================== +Age: 35 +You are an adult in 0 years +You can drink in 0 years +You can rent a car in 0 years +==================================== diff --git a/course/3-functions/exercises/6a-named_returns_explicit/readme.md b/course/3-functions/exercises/6a-named_returns_explicit/readme.md new file mode 100644 index 0000000..f51e5c1 --- /dev/null +++ b/course/3-functions/exercises/6a-named_returns_explicit/readme.md @@ -0,0 +1,29 @@ +# Named Return Values - Implicit Returns + +Even though a function has named return values, we can still explicitly return values if we want to. + +```go +func getCoords() (x, y int){ + return x, y // this is explicit +} +``` + +Using this explicit pattern we can even overwrite the return values: + +```go +func getCoords() (x, y int){ + return 5, 6 // this is explicit, x and y are NOT returned +} +``` + +Otherwise, if we want to return the values defined in the function signature we can just use a naked `return` (blank return): + +```go +func getCoords() (x, y int){ + return // implicitly returns x and y +} +``` + +## Assignment + +Fix the function to return the named values *implicitly*. diff --git a/course/3-functions/exercises/6b-when_to_name_returns/multiple_choice.json b/course/3-functions/exercises/6b-when_to_name_returns/multiple_choice.json new file mode 100644 index 0000000..27a8e18 --- /dev/null +++ b/course/3-functions/exercises/6b-when_to_name_returns/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "When should naked returns be used?", + "answers": [ + "For small functions", + "For large functions", + "For complex functions" + ] +} diff --git a/course/3-functions/exercises/6b-when_to_name_returns/readme.md b/course/3-functions/exercises/6b-when_to_name_returns/readme.md new file mode 100644 index 0000000..4c59db9 --- /dev/null +++ b/course/3-functions/exercises/6b-when_to_name_returns/readme.md @@ -0,0 +1,39 @@ +# The Benefits of Named Returns + +## Good For Documentation (Understanding) + +Named return parameters are great for documenting a function. We know what the function is returning directly from its signature, no need for a comment. + +Named return parameters are particularly important in longer functions with many return values. + +```go +func calculator(a, b int) (mul, div int, err error) { + if b == 0 { + return 0, 0, errors.New("Can't divide by zero") + } + mul = a * b + div = a / b + return mul, div, nil +} +``` + +Which is easier to understand than: + +```go +func calculator(a, b int) (int, int, error) { + if b == 0 { + return 0, 0, errors.New("Can't divide by zero") + } + mul := a * b + div := a / b + return mul, div, nil +} +``` + +We know *the meaning* of each return value just by looking at the function signature: `func calculator(a, b int) (mul, div int, err error)` + +## Less Code (Sometimes) + +If there are multiple return statements in a function, you don’t need to write all the return values each time, though you *probably* should. + +When you choose to omit return values, it's called a *naked* return. Naked returns should only be used in short and simple functions. diff --git a/course/3-functions/exercises/6c-when_to_name_returns/multiple_choice.json b/course/3-functions/exercises/6c-when_to_name_returns/multiple_choice.json new file mode 100644 index 0000000..bafa9a4 --- /dev/null +++ b/course/3-functions/exercises/6c-when_to_name_returns/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "When should named returns be used?", + "answers": [ + "When there are many values being returned", + "When there are few parameters being returned", + "When the function is simple" + ] +} diff --git a/course/3-functions/exercises/6c-when_to_name_returns/readme.md b/course/3-functions/exercises/6c-when_to_name_returns/readme.md new file mode 100644 index 0000000..4c59db9 --- /dev/null +++ b/course/3-functions/exercises/6c-when_to_name_returns/readme.md @@ -0,0 +1,39 @@ +# The Benefits of Named Returns + +## Good For Documentation (Understanding) + +Named return parameters are great for documenting a function. We know what the function is returning directly from its signature, no need for a comment. + +Named return parameters are particularly important in longer functions with many return values. + +```go +func calculator(a, b int) (mul, div int, err error) { + if b == 0 { + return 0, 0, errors.New("Can't divide by zero") + } + mul = a * b + div = a / b + return mul, div, nil +} +``` + +Which is easier to understand than: + +```go +func calculator(a, b int) (int, int, error) { + if b == 0 { + return 0, 0, errors.New("Can't divide by zero") + } + mul := a * b + div := a / b + return mul, div, nil +} +``` + +We know *the meaning* of each return value just by looking at the function signature: `func calculator(a, b int) (mul, div int, err error)` + +## Less Code (Sometimes) + +If there are multiple return statements in a function, you don’t need to write all the return values each time, though you *probably* should. + +When you choose to omit return values, it's called a *naked* return. Naked returns should only be used in short and simple functions. diff --git a/course/3-functions/exercises/7-early_returns/multiple_choice.json b/course/3-functions/exercises/7-early_returns/multiple_choice.json new file mode 100644 index 0000000..dd8d0fc --- /dev/null +++ b/course/3-functions/exercises/7-early_returns/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Which is true?", + "answers": [ + "Guard clauses provide a linear approach to logic trees", + "Guard clauses are unreadable", + "Guard clauses are generally worse than nested if/else statements" + ] +} diff --git a/course/3-functions/exercises/7-early_returns/readme.md b/course/3-functions/exercises/7-early_returns/readme.md new file mode 100644 index 0000000..b8260d6 --- /dev/null +++ b/course/3-functions/exercises/7-early_returns/readme.md @@ -0,0 +1,65 @@ +# Early Returns + +Go supports the ability to return early from a function. This is a powerful feature that can clean up code, especially when used as guard clauses. + +Guard Clauses leverage the ability to `return` early from a function (or `continue` through a loop) to make nested conditionals one-dimensional. Instead of using if/else chains, we just return early from the function at the end of each conditional block. + +```go +func divide(dividend, divisor int) (int, error) { + if divisor == 0 { + return 0, errors.New("Can't divide by zero") + } + return dividend/divisor, nil +} +``` + +Error handling in Go naturally encourages developers to make use of guard clauses. When I started writing more JavaScript, I was disappointed to see how many nested conditionals existed in the code I was working on. + +Let’s take a look at an exaggerated example of nested conditional logic: + +```go +func getInsuranceAmount(status insuranceStatus) int { + amount := 0 + if !status.hasInsurance(){ + amount = 1 + } else { + if status.isTotaled(){ + amount = 10000 + } else { + if status.isDented(){ + amount = 160 + if status.isBigDent(){ + amount = 270 + } + } else { + amount = 0 + } + } + } + return amount +} +``` + +This could be written with guard clauses instead: + +```go +func getInsuranceAmount(status insuranceStatus) int { + if !status.hasInsurance(){ + return 1 + } + if status.isTotaled(){ + return 10000 + } + if !status.isDented(){ + return 0 + } + if status.isBigDent(){ + return 270 + } + return 160 +} +``` + +The example above is *much* easier to read and understand. When writing code, it’s important to try to reduce the cognitive load on the reader by reducing the number of entities they need to think about at any given time. + +In the first example, if the developer is trying to figure out `when` 270 is returned, they need to think about each branch in the logic tree and try to remember which cases matter and which cases don’t. With the one-dimensional structure offered by guard clauses, it’s as simple as stepping through each case in order. diff --git a/course/3-functions/exercises/7a-early_returns/multiple_choice.json b/course/3-functions/exercises/7a-early_returns/multiple_choice.json new file mode 100644 index 0000000..7fc3787 --- /dev/null +++ b/course/3-functions/exercises/7a-early_returns/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What is a guard clause?", + "answers": [ + "An early return from a function when a given condition is met", + "An AND operation in boolean logic", + "A bitwise OR operation" + ] +} diff --git a/course/3-functions/exercises/7a-early_returns/readme.md b/course/3-functions/exercises/7a-early_returns/readme.md new file mode 100644 index 0000000..b8260d6 --- /dev/null +++ b/course/3-functions/exercises/7a-early_returns/readme.md @@ -0,0 +1,65 @@ +# Early Returns + +Go supports the ability to return early from a function. This is a powerful feature that can clean up code, especially when used as guard clauses. + +Guard Clauses leverage the ability to `return` early from a function (or `continue` through a loop) to make nested conditionals one-dimensional. Instead of using if/else chains, we just return early from the function at the end of each conditional block. + +```go +func divide(dividend, divisor int) (int, error) { + if divisor == 0 { + return 0, errors.New("Can't divide by zero") + } + return dividend/divisor, nil +} +``` + +Error handling in Go naturally encourages developers to make use of guard clauses. When I started writing more JavaScript, I was disappointed to see how many nested conditionals existed in the code I was working on. + +Let’s take a look at an exaggerated example of nested conditional logic: + +```go +func getInsuranceAmount(status insuranceStatus) int { + amount := 0 + if !status.hasInsurance(){ + amount = 1 + } else { + if status.isTotaled(){ + amount = 10000 + } else { + if status.isDented(){ + amount = 160 + if status.isBigDent(){ + amount = 270 + } + } else { + amount = 0 + } + } + } + return amount +} +``` + +This could be written with guard clauses instead: + +```go +func getInsuranceAmount(status insuranceStatus) int { + if !status.hasInsurance(){ + return 1 + } + if status.isTotaled(){ + return 10000 + } + if !status.isDented(){ + return 0 + } + if status.isBigDent(){ + return 270 + } + return 160 +} +``` + +The example above is *much* easier to read and understand. When writing code, it’s important to try to reduce the cognitive load on the reader by reducing the number of entities they need to think about at any given time. + +In the first example, if the developer is trying to figure out `when` 270 is returned, they need to think about each branch in the logic tree and try to remember which cases matter and which cases don’t. With the one-dimensional structure offered by guard clauses, it’s as simple as stepping through each case in order. diff --git a/course/4-structs/exercises/1-intro/code.go b/course/4-structs/exercises/1-intro/code.go new file mode 100644 index 0000000..b90358e --- /dev/null +++ b/course/4-structs/exercises/1-intro/code.go @@ -0,0 +1,28 @@ +package main + +import "fmt" + +type messageToSend struct { +} + +// don't edit below this line + +func test(m messageToSend) { + fmt.Printf("Sending message: '%s' to: %v\n", m.message, m.phoneNumber) + fmt.Println("====================================") +} + +func main() { + test(messageToSend{ + phoneNumber: 148255510981, + message: "Thanks for signing up", + }) + test(messageToSend{ + phoneNumber: 148255510982, + message: "Love to have you aboard!", + }) + test(messageToSend{ + phoneNumber: 148255510983, + message: "We're so excited to have you", + }) +} diff --git a/course/4-structs/exercises/1-intro/complete.go b/course/4-structs/exercises/1-intro/complete.go new file mode 100644 index 0000000..5a24693 --- /dev/null +++ b/course/4-structs/exercises/1-intro/complete.go @@ -0,0 +1,30 @@ +package main + +import "fmt" + +type messageToSend struct { + phoneNumber int + message string +} + +// don't edit below this line + +func test(m messageToSend) { + fmt.Printf("Sending message: '%s' to: %v\n", m.message, m.phoneNumber) + fmt.Println("====================================") +} + +func main() { + test(messageToSend{ + phoneNumber: 148255510981, + message: "Thanks for signing up", + }) + test(messageToSend{ + phoneNumber: 148255510982, + message: "Love to have you aboard!", + }) + test(messageToSend{ + phoneNumber: 148255510983, + message: "We're so excited to have you", + }) +} diff --git a/course/4-structs/exercises/1-intro/expected.txt b/course/4-structs/exercises/1-intro/expected.txt new file mode 100644 index 0000000..7607cfc --- /dev/null +++ b/course/4-structs/exercises/1-intro/expected.txt @@ -0,0 +1,6 @@ +Sending message: 'Thanks for signing up' to: 148255510981 +==================================== +Sending message: 'Love to have you aboard!' to: 148255510982 +==================================== +Sending message: 'We're so excited to have you' to: 148255510983 +==================================== diff --git a/course/4-structs/exercises/1-intro/readme.md b/course/4-structs/exercises/1-intro/readme.md new file mode 100644 index 0000000..4135f33 --- /dev/null +++ b/course/4-structs/exercises/1-intro/readme.md @@ -0,0 +1,23 @@ +# Structs in Go + +We use structs in Go to represent structured data. It's often convenient to group different types of variables together. For example, if we want to represent a car we could do the following: + +```go +type car struct { + Make string + Model string + Height int + Width int +} +``` + +This creates a new struct type called `car`. All cars have a `Make`, `Model`, `Height` and `Width`. + +In Go, you will often use a struct to represent information that you would have used a dictionary for in Python, or an object literal for in JavaScript. + +## Assignment + +Complete the `messageToSend` struct definition. It needs two fields: + +* `phoneNumber` - an integer +* `message` - a string. diff --git a/course/4-structs/exercises/2-nested_structs/code.go b/course/4-structs/exercises/2-nested_structs/code.go new file mode 100644 index 0000000..14c73c8 --- /dev/null +++ b/course/4-structs/exercises/2-nested_structs/code.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" +) + +type messageToSend struct { + message string + sender user + recipient user +} + +type user struct { + name string + number int +} + +func canSendMessage(mToSend messageToSend) bool { + // ? + return true +} + +// don't touch below this line + +func test(mToSend messageToSend) { + fmt.Printf(`sending "%s" from %s (%v) to %s (%v)...`, + mToSend.message, + mToSend.sender.name, + mToSend.sender.number, + mToSend.recipient.name, + mToSend.recipient.number, + ) + fmt.Println() + if canSendMessage(mToSend) { + fmt.Println("...sent!") + } else { + fmt.Println("...can't send message") + } + fmt.Println("====================================") +} + +func main() { + test(messageToSend{ + message: "you have an appointment tommorow", + sender: user{ + name: "Brenda Halafax", + number: 16545550987, + }, + recipient: user{ + name: "Sally Sue", + number: 19035558973, + }, + }) + test(messageToSend{ + message: "you have an event tommorow", + sender: user{ + number: 16545550987, + }, + recipient: user{ + name: "Suzie Sall", + number: 0, + }, + }) + test(messageToSend{ + message: "you have an party tommorow", + sender: user{ + name: "Njorn Halafax", + number: 16545550987, + }, + recipient: user{ + name: "Sally Sue", + number: 19035558973, + }, + }) + test(messageToSend{ + message: "you have a birthday tommorow", + sender: user{ + name: "Eli Halafax", + number: 0, + }, + recipient: user{ + name: "Whitaker Sue", + number: 19035558973, + }, + }) +} diff --git a/course/4-structs/exercises/2-nested_structs/complete.go b/course/4-structs/exercises/2-nested_structs/complete.go new file mode 100644 index 0000000..23cf322 --- /dev/null +++ b/course/4-structs/exercises/2-nested_structs/complete.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" +) + +type messageToSend struct { + message string + sender user + recipient user +} + +type user struct { + name string + number int +} + +func canSendMessage(mToSend messageToSend) bool { + if mToSend.recipient.number == 0 { + return false + } + if mToSend.sender.number == 0 { + return false + } + if mToSend.recipient.name == "" { + return false + } + if mToSend.sender.name == "" { + return false + } + return true +} + +// don't touch below this line + +func test(mToSend messageToSend) { + fmt.Printf(`sending "%s" from %s (%v) to %s (%v)...`, + mToSend.message, + mToSend.sender.name, + mToSend.sender.number, + mToSend.recipient.name, + mToSend.recipient.number, + ) + fmt.Println() + if canSendMessage(mToSend) { + fmt.Println("...sent!") + } else { + fmt.Println("...can't send message") + } + fmt.Println("====================================") +} + +func main() { + test(messageToSend{ + message: "you have an appointment tommorow", + sender: user{ + name: "Brenda Halafax", + number: 16545550987, + }, + recipient: user{ + name: "Sally Sue", + number: 19035558973, + }, + }) + test(messageToSend{ + message: "you have an event tommorow", + sender: user{ + number: 16545550987, + }, + recipient: user{ + name: "Suzie Sall", + number: 0, + }, + }) + test(messageToSend{ + message: "you have an party tommorow", + sender: user{ + name: "Njorn Halafax", + number: 16545550987, + }, + recipient: user{ + name: "Sally Sue", + number: 19035558973, + }, + }) + test(messageToSend{ + message: "you have a birthday tommorow", + sender: user{ + name: "Eli Halafax", + number: 0, + }, + recipient: user{ + name: "Whitaker Sue", + number: 19035558973, + }, + }) +} diff --git a/course/4-structs/exercises/2-nested_structs/expected.txt b/course/4-structs/exercises/2-nested_structs/expected.txt new file mode 100644 index 0000000..89b63b7 --- /dev/null +++ b/course/4-structs/exercises/2-nested_structs/expected.txt @@ -0,0 +1,12 @@ +sending "you have an appointment tommorow" from Brenda Halafax (16545550987) to Sally Sue (19035558973)... +...sent! +==================================== +sending "you have an event tommorow" from (16545550987) to Suzie Sall (0)... +...can't send message +==================================== +sending "you have an party tommorow" from Njorn Halafax (16545550987) to Sally Sue (19035558973)... +...sent! +==================================== +sending "you have a birthday tommorow" from Eli Halafax (0) to Whitaker Sue (19035558973)... +...can't send message +==================================== diff --git a/course/4-structs/exercises/2-nested_structs/readme.md b/course/4-structs/exercises/2-nested_structs/readme.md new file mode 100644 index 0000000..53e97dd --- /dev/null +++ b/course/4-structs/exercises/2-nested_structs/readme.md @@ -0,0 +1,34 @@ +# Nested structs in Go + +Structs can be nested to represent more complex entities: + +```go +type car struct { + Make string + Model string + Height int + Width int + FrontWheel Wheel + BackWheel Wheel +} + +type Wheel struct { + Radius int + Material string +} +``` + +The fields of a struct can be accessed using the dot `.` operator. + +```go +myCar := car{} +myCar.FrontWheel.Radius = 5 +``` + +## Assignment + +Textio has a bug, we've been sending texts with information missing! Before we send text messages in Textio, we should check to make sure the required fields have non-zero values. + +Notice that the `user` struct is a nested struct within the `messageToSend` struct. Both `sender` and `recipient` are `user` struct types. + +Complete the `canSendMessage` function. It should return `true` only if the `sender` and `recipient` fields each contain a `name` and a `number`. If any of the default zero values are present, return `false` instead. diff --git a/course/4-structs/exercises/3-anonymous_structs/multiple_choice.json b/course/4-structs/exercises/3-anonymous_structs/multiple_choice.json new file mode 100644 index 0000000..f26deab --- /dev/null +++ b/course/4-structs/exercises/3-anonymous_structs/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "What is a good reason to use an anonymous struct?", + "answers": [ + "It is only being used once", + "You're worried about user privacy", + "You're worried about security", + "You need your code to be faster" + ] +} diff --git a/course/4-structs/exercises/3-anonymous_structs/readme.md b/course/4-structs/exercises/3-anonymous_structs/readme.md new file mode 100644 index 0000000..d33173e --- /dev/null +++ b/course/4-structs/exercises/3-anonymous_structs/readme.md @@ -0,0 +1,39 @@ +# Anonymous Structs in Go + +An anonymous struct is just like a normal struct, but it is defined without a name and therefore cannot be referenced elsewhere in the code. + +To create an anonymous struct, just instantiate the instance immediately using a second pair of brackets after declaring the type: + +```go +myCar := struct { + Make string + Model string +} { + Make: "tesla", + Model: "model 3" +} +``` + +You can even nest anonymous structs as fields within other structs: + +```go +type car struct { + Make string + Model string + Height int + Width int + // Wheel is a field containing an anonymous struct + Wheel struct { + Radius int + Material string + } +} +``` + +## When should you use an anonymous struct? + +In general, *prefer named structs*. Named structs make it easier to read and understand your code, and they have the nice side-effect of being reusable. I sometimes use anonymous structs when I *know* I won't ever need to use a struct again. For example, sometimes I'll use one to create the shape of some JSON data in HTTP handlers. + +If a struct is only meant to be used once, then it makes sense to declare it in such a way that developers down the road won’t be tempted to accidentally use it again. + +You can read more about [anonymous structs here](https://blog.boot.dev/golang/anonymous-structs-golang/) if you're curious. diff --git a/course/4-structs/exercises/3a-anonymous_structs/multiple_choice.json b/course/4-structs/exercises/3a-anonymous_structs/multiple_choice.json new file mode 100644 index 0000000..3d6d7f4 --- /dev/null +++ b/course/4-structs/exercises/3a-anonymous_structs/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What's one advantage of using an anonymous struct?", + "answers": [ + "Anonymous structs prevent you from re-using a struct definition you never intended to re-use", + "Anonymous structs make your code run faster", + "Anonymous structs can be compiled more quickly" + ] +} diff --git a/course/4-structs/exercises/3a-anonymous_structs/readme.md b/course/4-structs/exercises/3a-anonymous_structs/readme.md new file mode 100644 index 0000000..d33173e --- /dev/null +++ b/course/4-structs/exercises/3a-anonymous_structs/readme.md @@ -0,0 +1,39 @@ +# Anonymous Structs in Go + +An anonymous struct is just like a normal struct, but it is defined without a name and therefore cannot be referenced elsewhere in the code. + +To create an anonymous struct, just instantiate the instance immediately using a second pair of brackets after declaring the type: + +```go +myCar := struct { + Make string + Model string +} { + Make: "tesla", + Model: "model 3" +} +``` + +You can even nest anonymous structs as fields within other structs: + +```go +type car struct { + Make string + Model string + Height int + Width int + // Wheel is a field containing an anonymous struct + Wheel struct { + Radius int + Material string + } +} +``` + +## When should you use an anonymous struct? + +In general, *prefer named structs*. Named structs make it easier to read and understand your code, and they have the nice side-effect of being reusable. I sometimes use anonymous structs when I *know* I won't ever need to use a struct again. For example, sometimes I'll use one to create the shape of some JSON data in HTTP handlers. + +If a struct is only meant to be used once, then it makes sense to declare it in such a way that developers down the road won’t be tempted to accidentally use it again. + +You can read more about [anonymous structs here](https://blog.boot.dev/golang/anonymous-structs-golang/) if you're curious. diff --git a/course/4-structs/exercises/4-embedded_structs/code.go b/course/4-structs/exercises/4-embedded_structs/code.go new file mode 100644 index 0000000..c624cc8 --- /dev/null +++ b/course/4-structs/exercises/4-embedded_structs/code.go @@ -0,0 +1,45 @@ +package main + +import "fmt" + +type sender struct { + rateLimit int +} + +type user struct { + name string + number int +} + +// don't edit below this line + +func test(s sender) { + fmt.Println("Sender name:", s.name) + fmt.Println("Sender number:", s.number) + fmt.Println("Sender rateLimit:", s.rateLimit) + fmt.Println("====================================") +} + +func main() { + test(sender{ + rateLimit: 10000, + user: user{ + name: "Deborah", + number: 18055558790, + }, + }) + test(sender{ + rateLimit: 5000, + user: user{ + name: "Sarah", + number: 19055558790, + }, + }) + test(sender{ + rateLimit: 1000, + user: user{ + name: "Sally", + number: 19055558790, + }, + }) +} diff --git a/course/4-structs/exercises/4-embedded_structs/complete.go b/course/4-structs/exercises/4-embedded_structs/complete.go new file mode 100644 index 0000000..1b5d8b6 --- /dev/null +++ b/course/4-structs/exercises/4-embedded_structs/complete.go @@ -0,0 +1,46 @@ +package main + +import "fmt" + +type sender struct { + rateLimit int + user +} + +type user struct { + name string + number int +} + +// don't edit below this line + +func test(s sender) { + fmt.Println("Sender name:", s.name) + fmt.Println("Sender number:", s.number) + fmt.Println("Sender rateLimit:", s.rateLimit) + fmt.Println("====================================") +} + +func main() { + test(sender{ + rateLimit: 10000, + user: user{ + name: "Deborah", + number: 18055558790, + }, + }) + test(sender{ + rateLimit: 5000, + user: user{ + name: "Sarah", + number: 19055558790, + }, + }) + test(sender{ + rateLimit: 1000, + user: user{ + name: "Sally", + number: 19055558790, + }, + }) +} diff --git a/course/4-structs/exercises/4-embedded_structs/expected.txt b/course/4-structs/exercises/4-embedded_structs/expected.txt new file mode 100644 index 0000000..a04f5cf --- /dev/null +++ b/course/4-structs/exercises/4-embedded_structs/expected.txt @@ -0,0 +1,12 @@ +Sender name: Deborah +Sender number: 18055558790 +Sender rateLimit: 10000 +==================================== +Sender name: Sarah +Sender number: 19055558790 +Sender rateLimit: 5000 +==================================== +Sender name: Sally +Sender number: 19055558790 +Sender rateLimit: 1000 +==================================== diff --git a/course/4-structs/exercises/4-embedded_structs/readme.md b/course/4-structs/exercises/4-embedded_structs/readme.md new file mode 100644 index 0000000..8f83445 --- /dev/null +++ b/course/4-structs/exercises/4-embedded_structs/readme.md @@ -0,0 +1,46 @@ +# Embedded Structs + +Go is not an [object-oriented](https://en.wikipedia.org/wiki/Object-oriented_programming) language. However, embedded structs provide a kind of *data-only* inheritance that can be useful at times. Keep in mind, Go doesn't support classes or inheritance in the complete sense, embedded structs are just a way to elevate and share fields between struct definitions. + +```go +type car struct { + make string + model string +} + +type truck struct { + // "car" is embedded, so the definition of a + // "truck" now also additionally contains all + // of the fields of the car struct + car + bedSize int +} +``` + +## Embedded vs nested + +* An embedded struct's fields are accessed at the top level, unlike nested structs. +* Promoted fields can be accessed like normal fields except that they can't be used in [composite literals](https://golang.org/ref/spec#Composite_literals) + +```go +lanesTruck := truck{ + bedSize: 10, + car: car{ + make: "toyota", + model: "camry", + }, +} + +fmt.Println(lanesTruck.bedSize) + +// embedded fields promoted to the top-level +// instead of lanesTruck.car.make +fmt.Println(lanesTruck.make) +fmt.Println(lanesTruck.model) +``` + +## Assignment + +At Textio, a "user" struct represents an account holder, and a "sender" is just a "user" with some "sender" specific data. A "sender" is a user that has a `rateLimit` field that tells us how many messages they are allowed to send. + +Fix the system by using an embedded struct as expected by the test code. diff --git a/course/4-structs/exercises/5-methods/code.go b/course/4-structs/exercises/5-methods/code.go new file mode 100644 index 0000000..a82da31 --- /dev/null +++ b/course/4-structs/exercises/5-methods/code.go @@ -0,0 +1,32 @@ +package main + +import "fmt" + +type authenticationInfo struct { + username string + password string +} + +// ? + +// don't touch below this line + +func test(authInfo authenticationInfo) { + fmt.Println(authInfo.getBasicAuth()) + fmt.Println("====================================") +} + +func main() { + test(authenticationInfo{ + username: "Google", + password: "12345", + }) + test(authenticationInfo{ + username: "Bing", + password: "98765", + }) + test(authenticationInfo{ + username: "DDG", + password: "76921", + }) +} diff --git a/course/4-structs/exercises/5-methods/complete.go b/course/4-structs/exercises/5-methods/complete.go new file mode 100644 index 0000000..db83dff --- /dev/null +++ b/course/4-structs/exercises/5-methods/complete.go @@ -0,0 +1,34 @@ +package main + +import "fmt" + +type authenticationInfo struct { + username string + password string +} + +func (authInfo authenticationInfo) getBasicAuth() string { + return "Authorization: Basic " + authInfo.username + ":" + authInfo.password +} + +// don't touch below this line + +func test(authInfo authenticationInfo) { + fmt.Println(authInfo.getBasicAuth()) + fmt.Println("====================================") +} + +func main() { + test(authenticationInfo{ + username: "Google", + password: "12345", + }) + test(authenticationInfo{ + username: "Bing", + password: "98765", + }) + test(authenticationInfo{ + username: "DDG", + password: "76921", + }) +} diff --git a/course/4-structs/exercises/5-methods/expected.txt b/course/4-structs/exercises/5-methods/expected.txt new file mode 100644 index 0000000..76e9081 --- /dev/null +++ b/course/4-structs/exercises/5-methods/expected.txt @@ -0,0 +1,6 @@ +Authorization: Basic Google:12345 +==================================== +Authorization: Basic Bing:98765 +==================================== +Authorization: Basic DDG:76921 +==================================== diff --git a/course/4-structs/exercises/5-methods/readme.md b/course/4-structs/exercises/5-methods/readme.md new file mode 100644 index 0000000..edd55cb --- /dev/null +++ b/course/4-structs/exercises/5-methods/readme.md @@ -0,0 +1,37 @@ +# Struct methods in Go + +While Go is **not** object-oriented, it does support methods that can be defined on structs. Methods are just functions that have a receiver. A receiver is a special parameter that syntactically goes *before* the name of the function. + +```go +type rect struct { + width int + height int +} + +// area has a receiver of (r rect) +func (r rect) area() int { + return r.width * r.height +} + +r := rect{ + width: 5, + height: 10, +} + +fmt.Println(r.area()) +// prints 50 +``` + +A receiver is just a special kind of function parameter. Receivers are important because they will, as you'll learn in the exercises to come, allow us to define interfaces that our structs (and other types) can implement. + +## Assignment + +Let's clean up Textio's authentication logic. We store our user's authentication data inside an `authenticationInfo` struct. We need a method that can take that data and return a basic authorization string. + +The format of the string should be: + +``` +Authorization: Basic USERNAME:PASSWORD +``` + +Create a method on the `authenticationInfo` struct called `getBasicAuth` that returns the formatted string. diff --git a/course/5-interfaces/exercises/1-interfaces/code.go b/course/5-interfaces/exercises/1-interfaces/code.go new file mode 100644 index 0000000..b1099bf --- /dev/null +++ b/course/5-interfaces/exercises/1-interfaces/code.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "time" +) + +func sendMessage(msg message) { + // ? +} + +type message interface { + // ? +} + +// don't edit below this line + +type birthdayMessage struct { + birthdayTime time.Time + recipientName string +} + +func (bm birthdayMessage) getMessage() string { + return fmt.Sprintf("Hi %s, it is your birthday on %s", bm.recipientName, bm.birthdayTime.Format(time.RFC3339)) +} + +type sendingReport struct { + reportName string + numberOfSends int +} + +func (sr sendingReport) getMessage() string { + return fmt.Sprintf(`Your "%s" report is ready. You've sent %v messages.`, sr.reportName, sr.numberOfSends) +} + +func test(m message) { + sendMessage(m) + fmt.Println("====================================") +} + +func main() { + test(sendingReport{ + reportName: "First Report", + numberOfSends: 10, + }) + test(birthdayMessage{ + recipientName: "John Doe", + birthdayTime: time.Date(1994, 03, 21, 0, 0, 0, 0, time.UTC), + }) + test(sendingReport{ + reportName: "First Report", + numberOfSends: 10, + }) + test(birthdayMessage{ + recipientName: "Bill Deer", + birthdayTime: time.Date(1934, 05, 01, 0, 0, 0, 0, time.UTC), + }) +} diff --git a/course/5-interfaces/exercises/1-interfaces/complete.go b/course/5-interfaces/exercises/1-interfaces/complete.go new file mode 100644 index 0000000..0d48a77 --- /dev/null +++ b/course/5-interfaces/exercises/1-interfaces/complete.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "time" +) + +func sendMessage(msg message) { + fmt.Println(msg.getMessage()) +} + +type message interface { + getMessage() string +} + +// don't edit below this line + +type birthdayMessage struct { + birthdayTime time.Time + recipientName string +} + +func (bm birthdayMessage) getMessage() string { + return fmt.Sprintf("Hi %s, it is your birthday on %s", bm.recipientName, bm.birthdayTime.Format(time.RFC3339)) +} + +type sendingReport struct { + reportName string + numberOfSends int +} + +func (sr sendingReport) getMessage() string { + return fmt.Sprintf(`Your "%s" report is ready. You've sent %v messages.`, sr.reportName, sr.numberOfSends) +} + +func test(m message) { + sendMessage(m) + fmt.Println("====================================") +} + +func main() { + test(sendingReport{ + reportName: "First Report", + numberOfSends: 10, + }) + test(birthdayMessage{ + recipientName: "John Doe", + birthdayTime: time.Date(1994, 03, 21, 0, 0, 0, 0, time.UTC), + }) + test(sendingReport{ + reportName: "First Report", + numberOfSends: 10, + }) + test(birthdayMessage{ + recipientName: "Bill Deer", + birthdayTime: time.Date(1934, 05, 01, 0, 0, 0, 0, time.UTC), + }) +} diff --git a/course/5-interfaces/exercises/1-interfaces/expected.txt b/course/5-interfaces/exercises/1-interfaces/expected.txt new file mode 100644 index 0000000..c92e93c --- /dev/null +++ b/course/5-interfaces/exercises/1-interfaces/expected.txt @@ -0,0 +1,8 @@ +Your "First Report" report is ready. You've sent 10 messages. +==================================== +Hi John Doe, it is your birthday on 1994-03-21T00:00:00Z +==================================== +Your "First Report" report is ready. You've sent 10 messages. +==================================== +Hi Bill Deer, it is your birthday on 1934-05-01T00:00:00Z +==================================== diff --git a/course/5-interfaces/exercises/1-interfaces/readme.md b/course/5-interfaces/exercises/1-interfaces/readme.md new file mode 100644 index 0000000..b829949 --- /dev/null +++ b/course/5-interfaces/exercises/1-interfaces/readme.md @@ -0,0 +1,42 @@ +# Interfaces in Go + +Interfaces are collections of method signatures. A type "implements" an interface if it has all of the methods of the given interface defined on it. + +In the following example, a "shape" must be able to return its area and perimeter. Both `rect` and `circle` fulfill the interface. + +```go +type shape interface { + area() float64 + perimeter() float64 +} + +type rect struct { + width, height float64 +} +func (r rect) area() float64 { + return r.width * r.height +} +func (r rect) perimeter() float64 { + return 2*r.width + 2*r.height +} + +type circle struct { + radius float64 +} +func (c circle) area() float64 { + return math.Pi * c.radius * c.radius +} +func (c circle) perimeter() float64 { + return 2 * math.Pi * c.radius +} +``` + +When a type implements an interface, it can then be used as the interface type. + +## Assignment + +The `birthdayMessage` and `sendingReport` structs have already implemented the `getMessage` methods. The `getMessage` method simply returns a string, and any type that implements the method can be considered a `message`. + +First, add the `getMessage()` method as a requirement on the method interface. + +Second, complete the `sendMessage` function. It should print a message's `message`, which it obtains through the interface method. Notice that your code doesn't need to worry *at all* about whether a specific message is a `birthdayMessage` or a `sendingReport`! diff --git a/course/5-interfaces/exercises/2-implements/code.go b/course/5-interfaces/exercises/2-implements/code.go new file mode 100644 index 0000000..dc28bec --- /dev/null +++ b/course/5-interfaces/exercises/2-implements/code.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" +) + +type employee interface { + getName() string + getSalary() int +} + +type contractor struct { + name string + hourlyPay int + hoursPerYear int +} + +func (c contractor) getName() string { + return c.name +} + +// ? + +// don't touch below this line + +type fullTime struct { + name string + salary int +} + +func (ft fullTime) getSalary() int { + return ft.salary +} + +func (ft fullTime) getName() string { + return ft.name +} + +func test(e employee) { + fmt.Println(e.getName(), e.getSalary()) + fmt.Println("====================================") +} + +func main() { + test(fullTime{ + name: "Jack", + salary: 50000, + }) + test(contractor{ + name: "Bob", + hourlyPay: 100, + hoursPerYear: 73, + }) + test(contractor{ + name: "Jill", + hourlyPay: 872, + hoursPerYear: 982, + }) +} diff --git a/course/5-interfaces/exercises/2-implements/complete.go b/course/5-interfaces/exercises/2-implements/complete.go new file mode 100644 index 0000000..4234e8a --- /dev/null +++ b/course/5-interfaces/exercises/2-implements/complete.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" +) + +type employee interface { + getName() string + getSalary() int +} + +type contractor struct { + name string + hourlyPay int + hoursPerYear int +} + +func (c contractor) getName() string { + return c.name +} + +func (c contractor) getSalary() int { + return c.hourlyPay * c.hoursPerYear +} + +// don't touch below this line + +type fullTime struct { + name string + salary int +} + +func (ft fullTime) getSalary() int { + return ft.salary +} + +func (ft fullTime) getName() string { + return ft.name +} + +func test(e employee) { + fmt.Println(e.getName(), e.getSalary()) + fmt.Println("====================================") +} + +func main() { + test(fullTime{ + name: "Jack", + salary: 50000, + }) + test(contractor{ + name: "Bob", + hourlyPay: 100, + hoursPerYear: 73, + }) + test(contractor{ + name: "Jill", + hourlyPay: 872, + hoursPerYear: 982, + }) +} diff --git a/course/5-interfaces/exercises/2-implements/expected.txt b/course/5-interfaces/exercises/2-implements/expected.txt new file mode 100644 index 0000000..4c405ba --- /dev/null +++ b/course/5-interfaces/exercises/2-implements/expected.txt @@ -0,0 +1,6 @@ +Jack 50000 +==================================== +Bob 7300 +==================================== +Jill 856304 +==================================== diff --git a/course/5-interfaces/exercises/2-implements/readme.md b/course/5-interfaces/exercises/2-implements/readme.md new file mode 100644 index 0000000..a14d9f2 --- /dev/null +++ b/course/5-interfaces/exercises/2-implements/readme.md @@ -0,0 +1,13 @@ +# Interface Implementation + +Interfaces are implemented *implicitly*. + +A type never declares that it implements a given interface. If an interface exists and a type has the proper methods defined, then the type automatically fulfills that interface. + +## Assignment + +At Textio we have full-time employees and contract employees. We have been tasked with making a more general `employee` interface so that dealing with different employee types is simpler. + +Add the missing `getSalary` method to the `contractor` type so that it fulfills the `employee` interface. + +A contractor's salary is their hourly pay multiplied by how many hours they work per year. diff --git a/course/5-interfaces/exercises/3-implicit/multiple_choice.json b/course/5-interfaces/exercises/3-implicit/multiple_choice.json new file mode 100644 index 0000000..607124a --- /dev/null +++ b/course/5-interfaces/exercises/3-implicit/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "How is an interface fulfilled?", + "answers": [ + "A type has all the required interface's methods defined on it", + "A struct embeds the interface in its definition" + ] +} diff --git a/course/5-interfaces/exercises/3-implicit/readme.md b/course/5-interfaces/exercises/3-implicit/readme.md new file mode 100644 index 0000000..45142fa --- /dev/null +++ b/course/5-interfaces/exercises/3-implicit/readme.md @@ -0,0 +1,5 @@ +# Interfaces are implemented implicitly + +A type implements an interface by implementing its methods. Unlike in many other languages, there is no explicit declaration of intent, there is no "implements" keyword. + +Implicit interfaces *decouple* the definition of an interface from its implementation. You may add methods to a type and in the process be unknowingly implementing various interfaces, and *that's okay*. diff --git a/course/5-interfaces/exercises/3a-implicit/multiple_choice.json b/course/5-interfaces/exercises/3a-implicit/multiple_choice.json new file mode 100644 index 0000000..fe76f6f --- /dev/null +++ b/course/5-interfaces/exercises/3a-implicit/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Can a type fulfill multiple interfaces?", + "answers": [ + "Yes, why not?", + "Never" + ] +} diff --git a/course/5-interfaces/exercises/3a-implicit/readme.md b/course/5-interfaces/exercises/3a-implicit/readme.md new file mode 100644 index 0000000..45142fa --- /dev/null +++ b/course/5-interfaces/exercises/3a-implicit/readme.md @@ -0,0 +1,5 @@ +# Interfaces are implemented implicitly + +A type implements an interface by implementing its methods. Unlike in many other languages, there is no explicit declaration of intent, there is no "implements" keyword. + +Implicit interfaces *decouple* the definition of an interface from its implementation. You may add methods to a type and in the process be unknowingly implementing various interfaces, and *that's okay*. diff --git a/course/5-interfaces/exercises/4-quiz/multiple_choice.json b/course/5-interfaces/exercises/4-quiz/multiple_choice.json new file mode 100644 index 0000000..757bdf0 --- /dev/null +++ b/course/5-interfaces/exercises/4-quiz/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Go uses the ____ keyword to show that a type implements an interface", + "answers": [ + "there is no keyword in Go", + "implements", + "fulfills", + "inherits" + ] +} diff --git a/course/5-interfaces/exercises/4-quiz/readme.md b/course/5-interfaces/exercises/4-quiz/readme.md new file mode 100644 index 0000000..55a5d4e --- /dev/null +++ b/course/5-interfaces/exercises/4-quiz/readme.md @@ -0,0 +1,27 @@ +# Interfaces Quiz + +Remember, interfaces are collections of method signatures. A type "implements" an interface if it has all of the methods of the given interface defined on it. + +```go +type shape interface { + area() float64 +} +``` + +If a type in your code implements an `area` method, with the same signature (e.g. accepts nothing and returns a `float64`), then that object is said to *implement* the `shape` interface. + +```go +type circle struct{ + radius int +} + +func (c *circle) area() float64 { + return 3.14 * c.radius * c.radius +} +``` + +This is *different from most other languages*, where you have to *explicitly* assign an interface type to an object, like with Java: + +```java +class Circle implements Shape +``` diff --git a/course/5-interfaces/exercises/4a-quiz/multiple_choice.json b/course/5-interfaces/exercises/4a-quiz/multiple_choice.json new file mode 100644 index 0000000..5268f52 --- /dev/null +++ b/course/5-interfaces/exercises/4a-quiz/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "In the example given, the ____ type implements the ____ interface", + "answers": [ + "circle, shape", + "shape, circle", + "circle, area", + "shape, area" + ] +} diff --git a/course/5-interfaces/exercises/4a-quiz/readme.md b/course/5-interfaces/exercises/4a-quiz/readme.md new file mode 100644 index 0000000..55a5d4e --- /dev/null +++ b/course/5-interfaces/exercises/4a-quiz/readme.md @@ -0,0 +1,27 @@ +# Interfaces Quiz + +Remember, interfaces are collections of method signatures. A type "implements" an interface if it has all of the methods of the given interface defined on it. + +```go +type shape interface { + area() float64 +} +``` + +If a type in your code implements an `area` method, with the same signature (e.g. accepts nothing and returns a `float64`), then that object is said to *implement* the `shape` interface. + +```go +type circle struct{ + radius int +} + +func (c *circle) area() float64 { + return 3.14 * c.radius * c.radius +} +``` + +This is *different from most other languages*, where you have to *explicitly* assign an interface type to an object, like with Java: + +```java +class Circle implements Shape +``` diff --git a/course/5-interfaces/exercises/5-multiple_interfaces/code.go b/course/5-interfaces/exercises/5-multiple_interfaces/code.go new file mode 100644 index 0000000..a8b1576 --- /dev/null +++ b/course/5-interfaces/exercises/5-multiple_interfaces/code.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" +) + +func (e email) cost() float64 { + // ? +} + +func (e email) print() { + // ? +} + +// don't touch below this line + +type expense interface { + cost() float64 +} + +type printer interface { + print() +} + +type email struct { + isSubscribed bool + body string +} + +func print(p printer) { + p.print() +} + +func test(e expense, p printer) { + fmt.Printf("Printing with cost: $%.2f ...\n", e.cost()) + p.print() + fmt.Println("====================================") +} + +func main() { + e := email{ + isSubscribed: true, + body: "hello there", + } + test(e, e) + e = email{ + isSubscribed: false, + body: "I want my money back", + } + test(e, e) + e = email{ + isSubscribed: true, + body: "Are you free for a chat?", + } + test(e, e) + e = email{ + isSubscribed: false, + body: "This meeting could have been an email", + } + test(e, e) +} diff --git a/course/5-interfaces/exercises/5-multiple_interfaces/complete.go b/course/5-interfaces/exercises/5-multiple_interfaces/complete.go new file mode 100644 index 0000000..67bc240 --- /dev/null +++ b/course/5-interfaces/exercises/5-multiple_interfaces/complete.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" +) + +func (e email) cost() float64 { + if !e.isSubscribed { + return float64(len(e.body)) * .05 + } + return float64(len(e.body)) * .01 +} + +func (e email) print() { + fmt.Println(e.body) +} + +// don't touch below this line + +type expense interface { + cost() float64 +} + +type printer interface { + print() +} + +type email struct { + isSubscribed bool + body string +} + +func print(p printer) { + p.print() +} + +func test(e expense, p printer) { + fmt.Printf("Printing with cost: $%.2f ...\n", e.cost()) + p.print() + fmt.Println("====================================") +} + +func main() { + e := email{ + isSubscribed: true, + body: "hello there", + } + test(e, e) + e = email{ + isSubscribed: false, + body: "I want my money back", + } + test(e, e) + e = email{ + isSubscribed: true, + body: "Are you free for a chat?", + } + test(e, e) + e = email{ + isSubscribed: false, + body: "This meeting could have been an email", + } + test(e, e) +} diff --git a/course/5-interfaces/exercises/5-multiple_interfaces/expected.txt b/course/5-interfaces/exercises/5-multiple_interfaces/expected.txt new file mode 100644 index 0000000..9cd8551 --- /dev/null +++ b/course/5-interfaces/exercises/5-multiple_interfaces/expected.txt @@ -0,0 +1,12 @@ +Printing with cost: $0.11 ... +hello there +==================================== +Printing with cost: $1.00 ... +I want my money back +==================================== +Printing with cost: $0.24 ... +Are you free for a chat? +==================================== +Printing with cost: $1.85 ... +This meeting could have been an email +==================================== diff --git a/course/5-interfaces/exercises/5-multiple_interfaces/readme.md b/course/5-interfaces/exercises/5-multiple_interfaces/readme.md new file mode 100644 index 0000000..92e69a6 --- /dev/null +++ b/course/5-interfaces/exercises/5-multiple_interfaces/readme.md @@ -0,0 +1,15 @@ +# Multiple Interfaces + +A type can implement any number of interfaces in Go. For example, the empty interface, `interface{}`, is *always* implemented by every type because it has no requirements. + +## Assignment + +Add the required methods so that the `email` type implements both the `expense` and `printer` interfaces. + +### cost() + +If the email is *not* "subscribed", then the cost is `0.05` for each character in the body. If it *is*, then the cost is `0.01` per character. + +### print() + +The `print` method should print to standard out the email's body text. diff --git a/course/5-interfaces/exercises/6-naming_args/multiple_choice.json b/course/5-interfaces/exercises/6-naming_args/multiple_choice.json new file mode 100644 index 0000000..3331e9a --- /dev/null +++ b/course/5-interfaces/exercises/6-naming_args/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Are you required to name the arguments of an interface in order for your code to compile properly?", + "answers": [ + "No", + "Yes" + ] +} diff --git a/course/5-interfaces/exercises/6-naming_args/readme.md b/course/5-interfaces/exercises/6-naming_args/readme.md new file mode 100644 index 0000000..217ddd9 --- /dev/null +++ b/course/5-interfaces/exercises/6-naming_args/readme.md @@ -0,0 +1,23 @@ +# Name Your Interface Arguments + +Consider the following interface: + +```go +type Copier interface { + Copy(string, string) int +} +``` + +Based on the code alone, can you deduce what *kinds* of strings you should pass into the `Copy` function? + +We know the function signature expects 2 string types, but what are they? Filenames? URLs? Raw string data? For that matter, what the heck is that `int` that's being returned? + +Let's add some named arguments and return data to make it more clear. + +```go +type Copier interface { + Copy(sourceFile string, destinationFile string) (bytesCopied int) +} +``` + +Much better. We can see what the expectations are now. The first argument is the `sourceFile`, the second argument is the `destinationFile`, and `bytesCopied`, an integer, is returned. diff --git a/course/5-interfaces/exercises/6a-naming_args/multiple_choice.json b/course/5-interfaces/exercises/6a-naming_args/multiple_choice.json new file mode 100644 index 0000000..bca4542 --- /dev/null +++ b/course/5-interfaces/exercises/6a-naming_args/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Why would you name your interface's method's parameters?", + "answers": [ + "Readability and clarity", + "Execution speed", + "Memory savings" + ] +} diff --git a/course/5-interfaces/exercises/6a-naming_args/readme.md b/course/5-interfaces/exercises/6a-naming_args/readme.md new file mode 100644 index 0000000..217ddd9 --- /dev/null +++ b/course/5-interfaces/exercises/6a-naming_args/readme.md @@ -0,0 +1,23 @@ +# Name Your Interface Arguments + +Consider the following interface: + +```go +type Copier interface { + Copy(string, string) int +} +``` + +Based on the code alone, can you deduce what *kinds* of strings you should pass into the `Copy` function? + +We know the function signature expects 2 string types, but what are they? Filenames? URLs? Raw string data? For that matter, what the heck is that `int` that's being returned? + +Let's add some named arguments and return data to make it more clear. + +```go +type Copier interface { + Copy(sourceFile string, destinationFile string) (bytesCopied int) +} +``` + +Much better. We can see what the expectations are now. The first argument is the `sourceFile`, the second argument is the `destinationFile`, and `bytesCopied`, an integer, is returned. diff --git a/course/5-interfaces/exercises/7-type_assertion/code.go b/course/5-interfaces/exercises/7-type_assertion/code.go new file mode 100644 index 0000000..a78986b --- /dev/null +++ b/course/5-interfaces/exercises/7-type_assertion/code.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" +) + +func getExpenseReport(e expense) (string, float64) { + // ? +} + +// don't touch below this line + +func (e email) cost() float64 { + if !e.isSubscribed { + return float64(len(e.body)) * .05 + } + return float64(len(e.body)) * .01 +} + +func (s sms) cost() float64 { + if !s.isSubscribed { + return float64(len(s.body)) * .1 + } + return float64(len(s.body)) * .03 +} + +func (i invalid) cost() float64 { + return 0.0 +} + +type expense interface { + cost() float64 +} + +type email struct { + isSubscribed bool + body string + toAddress string +} + +type sms struct { + isSubscribed bool + body string + toPhoneNumber string +} + +type invalid struct{} + +func estimateYearlyCost(e expense, averageMessagesPerYear int) float64 { + return e.cost() * float64(averageMessagesPerYear) +} + +func test(e expense) { + address, cost := getExpenseReport(e) + switch e.(type) { + case email: + fmt.Printf("Report: The email going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + case sms: + fmt.Printf("Report: The sms going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + default: + fmt.Println("Report: Invalid expense") + fmt.Println("====================================") + } +} + +func main() { + test(email{ + isSubscribed: true, + body: "hello there", + toAddress: "john@does.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "jane@doe.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "elon@doe.com", + }) + test(sms{ + isSubscribed: false, + body: "This meeting could have been an email", + toPhoneNumber: "+155555509832", + }) + test(sms{ + isSubscribed: false, + body: "This meeting could have been an email", + toPhoneNumber: "+155555504444", + }) + test(invalid{}) +} diff --git a/course/5-interfaces/exercises/7-type_assertion/complete.go b/course/5-interfaces/exercises/7-type_assertion/complete.go new file mode 100644 index 0000000..77bb4e3 --- /dev/null +++ b/course/5-interfaces/exercises/7-type_assertion/complete.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" +) + +func getExpenseReport(e expense) (string, float64) { + em, ok := e.(email) + if ok { + return em.toAddress, em.cost() + } + + sm, ok := e.(sms) + if ok { + return sm.toPhoneNumber, sm.cost() + } + + return "", 0.0 +} + +// don't touch below this line + +func (e email) cost() float64 { + if !e.isSubscribed { + return float64(len(e.body)) * .05 + } + return float64(len(e.body)) * .01 +} + +func (s sms) cost() float64 { + if !s.isSubscribed { + return float64(len(s.body)) * .1 + } + return float64(len(s.body)) * .03 +} + +func (i invalid) cost() float64 { + return 0.0 +} + +type expense interface { + cost() float64 +} + +type email struct { + isSubscribed bool + body string + toAddress string +} + +type sms struct { + isSubscribed bool + body string + toPhoneNumber string +} + +type invalid struct{} + +func estimateYearlyCost(e expense, averageMessagesPerYear int) float64 { + return e.cost() * float64(averageMessagesPerYear) +} + +func test(e expense) { + address, cost := getExpenseReport(e) + switch e.(type) { + case email: + fmt.Printf("Report: The email going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + case sms: + fmt.Printf("Report: The sms going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + default: + fmt.Println("Report: Invalid expense") + fmt.Println("====================================") + } +} + +func main() { + test(email{ + isSubscribed: true, + body: "hello there", + toAddress: "john@does.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "jane@doe.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "elon@doe.com", + }) + test(sms{ + isSubscribed: false, + body: "This meeting could have been an email", + toPhoneNumber: "+155555509832", + }) + test(sms{ + isSubscribed: false, + body: "This meeting could have been an email", + toPhoneNumber: "+155555504444", + }) + test(invalid{}) +} diff --git a/course/5-interfaces/exercises/7-type_assertion/expected.txt b/course/5-interfaces/exercises/7-type_assertion/expected.txt new file mode 100644 index 0000000..67adc63 --- /dev/null +++ b/course/5-interfaces/exercises/7-type_assertion/expected.txt @@ -0,0 +1,12 @@ +Report: The email going to john@does.com will cost: 0.11 +==================================== +Report: The email going to jane@doe.com will cost: 1.85 +==================================== +Report: The email going to elon@doe.com will cost: 1.85 +==================================== +Report: The sms going to +155555509832 will cost: 3.70 +==================================== +Report: The sms going to +155555504444 will cost: 3.70 +==================================== +Report: Invalid expense +==================================== diff --git a/course/5-interfaces/exercises/7-type_assertion/readme.md b/course/5-interfaces/exercises/7-type_assertion/readme.md new file mode 100644 index 0000000..cc99562 --- /dev/null +++ b/course/5-interfaces/exercises/7-type_assertion/readme.md @@ -0,0 +1,27 @@ +# Type assertions in Go + +When working with interfaces in Go, every once-in-awhile you'll need access to the underlying type of an interface value. You can cast an interface to its underlying type using a *type assertion*. + +```go +type shape interface { + area() float64 +} + +type circle struct { + radius float64 +} + +// "c" is a new circle cast from "s" +// which is an instance of a shape. +// "ok" is a bool that is true if s was a circle +// or false if s isn't a circle +c, ok := s.(circle) +``` + +## Assignment + +Implement the `getExpenseReport` function. + +* If the `expense` is an `email` then it should return the email's `toAddress` and the `cost` of the email. +* If the `expense` is an `sms` then it should return the sms's `toPhoneNumber` and its `cost`. +* If the expense has any other underlying type, just return an empty string and `0.0` for the cost. diff --git a/course/5-interfaces/exercises/8-type_switch/code.go b/course/5-interfaces/exercises/8-type_switch/code.go new file mode 100644 index 0000000..6d22e3b --- /dev/null +++ b/course/5-interfaces/exercises/8-type_switch/code.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" +) + +func getExpenseReport(e expense) (string, float64) { + // ? +} + +// don't touch below this line + +func (e email) cost() float64 { + if !e.isSubscribed { + return float64(len(e.body)) * .05 + } + return float64(len(e.body)) * .01 +} + +func (s sms) cost() float64 { + if !s.isSubscribed { + return float64(len(s.body)) * .1 + } + return float64(len(s.body)) * .03 +} + +func (i invalid) cost() float64 { + return 0.0 +} + +type expense interface { + cost() float64 +} + +type email struct { + isSubscribed bool + body string + toAddress string +} + +type sms struct { + isSubscribed bool + body string + toPhoneNumber string +} + +type invalid struct{} + +func estimateYearlyCost(e expense, averageMessagesPerYear int) float64 { + return e.cost() * float64(averageMessagesPerYear) +} + +func test(e expense) { + address, cost := getExpenseReport(e) + switch e.(type) { + case email: + fmt.Printf("Report: The email going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + case sms: + fmt.Printf("Report: The sms going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + default: + fmt.Println("Report: Invalid expense") + fmt.Println("====================================") + } +} + +func main() { + test(email{ + isSubscribed: true, + body: "hello there", + toAddress: "john@does.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "jane@doe.com", + }) + test(email{ + isSubscribed: false, + body: "Wanna catch up later?", + toAddress: "elon@doe.com", + }) + test(sms{ + isSubscribed: false, + body: "I'm a Nigerian prince, please send me your bank info so I can deposit $1000 dollars", + toPhoneNumber: "+155555509832", + }) + test(sms{ + isSubscribed: false, + body: "I don't need this", + toPhoneNumber: "+155555504444", + }) + test(invalid{}) +} diff --git a/course/5-interfaces/exercises/8-type_switch/complete.go b/course/5-interfaces/exercises/8-type_switch/complete.go new file mode 100644 index 0000000..8ac6fea --- /dev/null +++ b/course/5-interfaces/exercises/8-type_switch/complete.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" +) + +func getExpenseReport(e expense) (string, float64) { + switch v := e.(type) { + case email: + return v.toAddress, v.cost() + case sms: + return v.toPhoneNumber, v.cost() + default: + return "", 0.0 + } +} + +// don't touch below this line + +func (e email) cost() float64 { + if !e.isSubscribed { + return float64(len(e.body)) * .05 + } + return float64(len(e.body)) * .01 +} + +func (s sms) cost() float64 { + if !s.isSubscribed { + return float64(len(s.body)) * .1 + } + return float64(len(s.body)) * .03 +} + +func (i invalid) cost() float64 { + return 0.0 +} + +type expense interface { + cost() float64 +} + +type email struct { + isSubscribed bool + body string + toAddress string +} + +type sms struct { + isSubscribed bool + body string + toPhoneNumber string +} + +type invalid struct{} + +func estimateYearlyCost(e expense, averageMessagesPerYear int) float64 { + return e.cost() * float64(averageMessagesPerYear) +} + +func test(e expense) { + address, cost := getExpenseReport(e) + switch e.(type) { + case email: + fmt.Printf("Report: The email going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + case sms: + fmt.Printf("Report: The sms going to %s will cost: %.2f\n", address, cost) + fmt.Println("====================================") + default: + fmt.Println("Report: Invalid expense") + fmt.Println("====================================") + } +} + +func main() { + test(email{ + isSubscribed: true, + body: "hello there", + toAddress: "john@does.com", + }) + test(email{ + isSubscribed: false, + body: "This meeting could have been an email", + toAddress: "jane@doe.com", + }) + test(email{ + isSubscribed: false, + body: "Wanna catch up later?", + toAddress: "elon@doe.com", + }) + test(sms{ + isSubscribed: false, + body: "I'm a Nigerian prince, please send me your bank info so I can deposit $1000 dollars", + toPhoneNumber: "+155555509832", + }) + test(sms{ + isSubscribed: false, + body: "I don't need this", + toPhoneNumber: "+155555504444", + }) + test(invalid{}) +} diff --git a/course/5-interfaces/exercises/8-type_switch/expected.txt b/course/5-interfaces/exercises/8-type_switch/expected.txt new file mode 100644 index 0000000..711dee1 --- /dev/null +++ b/course/5-interfaces/exercises/8-type_switch/expected.txt @@ -0,0 +1,12 @@ +Report: The email going to john@does.com will cost: 0.11 +==================================== +Report: The email going to jane@doe.com will cost: 1.85 +==================================== +Report: The email going to elon@doe.com will cost: 1.05 +==================================== +Report: The sms going to +155555509832 will cost: 8.30 +==================================== +Report: The sms going to +155555504444 will cost: 1.70 +==================================== +Report: Invalid expense +==================================== diff --git a/course/5-interfaces/exercises/8-type_switch/readme.md b/course/5-interfaces/exercises/8-type_switch/readme.md new file mode 100644 index 0000000..4285f70 --- /dev/null +++ b/course/5-interfaces/exercises/8-type_switch/readme.md @@ -0,0 +1,41 @@ +# Type Switches + +A *type switch* makes it easy to do several type assertions in a series. + +A type switch is similar to a regular switch statement, but the cases specify *types* instead of *values*. + +```go +func printNumericValue(num interface{}) { + switch v := num.(type) { + case int: + fmt.Printf("%T\n", v) + case string: + fmt.Printf("%T\n", v) + default: + fmt.Printf("%T\n", v) + } +} + +func main() { + printNumericValue(1) + // prints "int" + + printNumericValue("1") + // prints "string" + + printNumericValue(struct{}{}) + // prints "struct {}" +} +``` + +`fmt.Printf("%T\n", v)` prints the *type* of a variable. + +## Assignment + +After submitting our last snippet of code for review, a more experienced gopher told us to use a type switch instead of successive assertions. Let's make that improvement! + +Implement the `getExpenseReport` function *using a type switch*. + +* If the `expense` is an `email` then it should return the email's `toAddress` and the `cost` of the email. +* If the `expense` is an `sms` then it should return the sms's `toPhoneNumber` and its `cost`. +* If the expense has any other underlying type, just return an empty string and `0.0` for the cost. diff --git a/course/5-interfaces/exercises/9-clean_interfaces/multiple_choice.json b/course/5-interfaces/exercises/9-clean_interfaces/multiple_choice.json new file mode 100644 index 0000000..7b67825 --- /dev/null +++ b/course/5-interfaces/exercises/9-clean_interfaces/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Interfaces should have as _ methods as possible", + "answers": [ + "Few", + "Many", + "Complex" + ] +} diff --git a/course/5-interfaces/exercises/9-clean_interfaces/readme.md b/course/5-interfaces/exercises/9-clean_interfaces/readme.md new file mode 100644 index 0000000..49e7890 --- /dev/null +++ b/course/5-interfaces/exercises/9-clean_interfaces/readme.md @@ -0,0 +1,59 @@ +# Clean Interfaces + +Writing clean interfaces is *hard*. Frankly, anytime you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean. + +## 1. Keep Interfaces Small + +If there is only one piece of advice that you take away from this article, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept. + +Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior: + +```go +type File interface { + io.Closer + io.Reader + io.Seeker + Readdir(count int) ([]os.FileInfo, error) + Stat() (os.FileInfo, error) +} +``` + +Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a *File*. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple `[]byte`. + +## 2. Interfaces Should Have No Knowledge of Satisfying Types + +An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time. + +For example, let’s assume we are building an interface to describe the components necessary to define a car. + +```go +type car interface { + Color() string + Speed() int + IsFiretruck() bool +} +``` + +`Color()` and `Speed()` make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. In order for this pattern to make any amount of sense, we would need a whole list of possible subtypes. `IsPickup()`, `IsSedan()`, `IsTank()`… where does it end?? + +Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as: + +```go +type firetruck interface { + car + HoseLength() int +} +``` + +Which inherits the required methods from `car` and adds one additional required method to make the `car` a `firetruck`. + +## 3. Interfaces Are Not Classes + +* Interfaces are not classes, they are slimmer. +* Interfaces don’t have constructors or deconstructors that require that data is created or destroyed. +* Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces. +* Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the `fmt.Stringer` interface, they all need their own version of the `String()` function. + +## Optional: Further reading + +[Best Practices for Interfaces in Go](https://blog.boot.dev/golang/golang-interfaces/) diff --git a/course/5-interfaces/exercises/9a-clean_interfaces/multiple_choice.json b/course/5-interfaces/exercises/9a-clean_interfaces/multiple_choice.json new file mode 100644 index 0000000..9dd8992 --- /dev/null +++ b/course/5-interfaces/exercises/9a-clean_interfaces/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "It's okay for types to be aware of the interfaces they satisfy", + "answers": [ + "True", + "False" + ] +} diff --git a/course/5-interfaces/exercises/9a-clean_interfaces/readme.md b/course/5-interfaces/exercises/9a-clean_interfaces/readme.md new file mode 100644 index 0000000..ed25586 --- /dev/null +++ b/course/5-interfaces/exercises/9a-clean_interfaces/readme.md @@ -0,0 +1,59 @@ +# Clean Interfaces + +Writing clean interfaces is *hard*. Frankly, anytime you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean. + +## 1. Keep Interfaces Small + +If there is only one piece of advice that you take away from this article, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept. + +Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior: + +```go +type File interface { + io.Closer + io.Reader + io.Seeker + Readdir(count int) ([]os.FileInfo, error) + Stat() (os.FileInfo, error) +} +``` + +Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a *File*. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple `[]byte`. + +## 2. Interfaces Should Have No Knowledge of Satisfying Types + +An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time. + +For example, let’s assume we are building an interface to describe the components necessary to define a car. + +```go +type car interface { + Color() string + Speed() int + IsFiretruck() bool +} +``` + +`Color()` and `Speed()` make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. For this pattern to make any amount of sense, we would need a whole list of possible subtypes. `IsPickup()`, `IsSedan()`, `IsTank()`… where does it end?? + +Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as: + +```go +type firetruck interface { + car + HoseLength() int +} +``` + +Which inherits the required methods from `car` and adds one additional required method to make the `car` a `firetruck`. + +## 3. Interfaces Are Not Classes + +* Interfaces are not classes, they are slimmer. +* Interfaces don’t have constructors or deconstructors that require that data is created or destroyed. +* Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces. +* Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the `fmt.Stringer` interface, they all need their own version of the `String()` function. + +## Optional: Further reading + +[Best Practices for Interfaces in Go](https://blog.boot.dev/golang/golang-interfaces/) diff --git a/course/5-interfaces/exercises/9b-clean_interfaces/multiple_choice.json b/course/5-interfaces/exercises/9b-clean_interfaces/multiple_choice.json new file mode 100644 index 0000000..d6ad8fd --- /dev/null +++ b/course/5-interfaces/exercises/9b-clean_interfaces/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "It's okay for interfaces to be aware of the types that satisfy them", + "answers": [ + "False", + "True" + ] +} diff --git a/course/5-interfaces/exercises/9b-clean_interfaces/readme.md b/course/5-interfaces/exercises/9b-clean_interfaces/readme.md new file mode 100644 index 0000000..ed25586 --- /dev/null +++ b/course/5-interfaces/exercises/9b-clean_interfaces/readme.md @@ -0,0 +1,59 @@ +# Clean Interfaces + +Writing clean interfaces is *hard*. Frankly, anytime you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean. + +## 1. Keep Interfaces Small + +If there is only one piece of advice that you take away from this article, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept. + +Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior: + +```go +type File interface { + io.Closer + io.Reader + io.Seeker + Readdir(count int) ([]os.FileInfo, error) + Stat() (os.FileInfo, error) +} +``` + +Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a *File*. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple `[]byte`. + +## 2. Interfaces Should Have No Knowledge of Satisfying Types + +An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time. + +For example, let’s assume we are building an interface to describe the components necessary to define a car. + +```go +type car interface { + Color() string + Speed() int + IsFiretruck() bool +} +``` + +`Color()` and `Speed()` make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. For this pattern to make any amount of sense, we would need a whole list of possible subtypes. `IsPickup()`, `IsSedan()`, `IsTank()`… where does it end?? + +Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as: + +```go +type firetruck interface { + car + HoseLength() int +} +``` + +Which inherits the required methods from `car` and adds one additional required method to make the `car` a `firetruck`. + +## 3. Interfaces Are Not Classes + +* Interfaces are not classes, they are slimmer. +* Interfaces don’t have constructors or deconstructors that require that data is created or destroyed. +* Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces. +* Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the `fmt.Stringer` interface, they all need their own version of the `String()` function. + +## Optional: Further reading + +[Best Practices for Interfaces in Go](https://blog.boot.dev/golang/golang-interfaces/) diff --git a/course/5-interfaces/exercises/9c-clean_interfaces/multiple_choice.json b/course/5-interfaces/exercises/9c-clean_interfaces/multiple_choice.json new file mode 100644 index 0000000..30c6bad --- /dev/null +++ b/course/5-interfaces/exercises/9c-clean_interfaces/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Interfaces allow you to define a method's behavior once and use it for many different types", + "answers": [ + "False", + "True" + ] +} diff --git a/course/5-interfaces/exercises/9c-clean_interfaces/readme.md b/course/5-interfaces/exercises/9c-clean_interfaces/readme.md new file mode 100644 index 0000000..ed25586 --- /dev/null +++ b/course/5-interfaces/exercises/9c-clean_interfaces/readme.md @@ -0,0 +1,59 @@ +# Clean Interfaces + +Writing clean interfaces is *hard*. Frankly, anytime you’re dealing with abstractions in code, the simple can become complex very quickly if you’re not careful. Let’s go over some rules of thumb for keeping interfaces clean. + +## 1. Keep Interfaces Small + +If there is only one piece of advice that you take away from this article, make it this: keep interfaces small! Interfaces are meant to define the minimal behavior necessary to accurately represent an idea or concept. + +Here is an example from the standard HTTP package of a larger interface that’s a good example of defining minimal behavior: + +```go +type File interface { + io.Closer + io.Reader + io.Seeker + Readdir(count int) ([]os.FileInfo, error) + Stat() (os.FileInfo, error) +} +``` + +Any type that satisfies the interface’s behaviors can be considered by the HTTP package as a *File*. This is convenient because the HTTP package doesn’t need to know if it’s dealing with a file on disk, a network buffer, or a simple `[]byte`. + +## 2. Interfaces Should Have No Knowledge of Satisfying Types + +An interface should define what is necessary for other types to classify as a member of that interface. They shouldn’t be aware of any types that happen to satisfy the interface at design time. + +For example, let’s assume we are building an interface to describe the components necessary to define a car. + +```go +type car interface { + Color() string + Speed() int + IsFiretruck() bool +} +``` + +`Color()` and `Speed()` make perfect sense, they are methods confined to the scope of a car. IsFiretruck() is an anti-pattern. We are forcing all cars to declare whether or not they are firetrucks. For this pattern to make any amount of sense, we would need a whole list of possible subtypes. `IsPickup()`, `IsSedan()`, `IsTank()`… where does it end?? + +Instead, the developer should have relied on the native functionality of type assertion to derive the underlying type when given an instance of the car interface. Or, if a sub-interface is needed, it can be defined as: + +```go +type firetruck interface { + car + HoseLength() int +} +``` + +Which inherits the required methods from `car` and adds one additional required method to make the `car` a `firetruck`. + +## 3. Interfaces Are Not Classes + +* Interfaces are not classes, they are slimmer. +* Interfaces don’t have constructors or deconstructors that require that data is created or destroyed. +* Interfaces aren’t hierarchical by nature, though there is syntactic sugar to create interfaces that happen to be supersets of other interfaces. +* Interfaces define function signatures, but not underlying behavior. Making an interface often won’t DRY up your code in regards to struct methods. For example, if five types satisfy the `fmt.Stringer` interface, they all need their own version of the `String()` function. + +## Optional: Further reading + +[Best Practices for Interfaces in Go](https://blog.boot.dev/golang/golang-interfaces/) diff --git a/course/6-errors/exercises/1-errors/code.go b/course/6-errors/exercises/1-errors/code.go new file mode 100644 index 0000000..f517dbd --- /dev/null +++ b/course/6-errors/exercises/1-errors/code.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" +) + +func sendSMSToCouple(msgToCustomer, msgToSpouse string) (float64, error) { + // ? +} + +// don't edit below this line + +func sendSMS(message string) (float64, error) { + const maxTextLen = 25 + const costPerChar = .0002 + if len(message) > maxTextLen { + return 0.0, fmt.Errorf("can't send texts over %v characters", maxTextLen) + } + return costPerChar * float64(len(message)), nil +} + +func test(msgToCustomer, msgToSpouse string) { + defer fmt.Println("========") + fmt.Println("Message for customer:", msgToCustomer) + fmt.Println("Message for spouse:", msgToSpouse) + totalCost, err := sendSMSToCouple(msgToCustomer, msgToSpouse) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Printf("Total cost: $%.4f\n", totalCost) +} + +func main() { + test( + "Thanks for coming in to our flower shop today!", + "We hope you enjoyed your gift.", + ) + test( + "Thanks for joining us!", + "Have a good day.", + ) + test( + "Thank you.", + "Enjoy!", + ) + test( + "We loved having you in!", + "We hope the rest of your evening is absolutely fantastic.", + ) +} diff --git a/course/6-errors/exercises/1-errors/complete.go b/course/6-errors/exercises/1-errors/complete.go new file mode 100644 index 0000000..10185fe --- /dev/null +++ b/course/6-errors/exercises/1-errors/complete.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" +) + +func sendSMSToCouple(msgToCustomer, msgToSpouse string) (float64, error) { + cost, err := sendSMS(msgToCustomer) + if err != nil { + return 0.0, err + } + + costSpouse, err := sendSMS(msgToSpouse) + if err != nil { + return 0, err + } + return costSpouse + cost, nil +} + +// don't edit below this line + +func sendSMS(message string) (float64, error) { + const maxTextLen = 25 + const costPerChar = .0002 + if len(message) > maxTextLen { + return 0.0, fmt.Errorf("can't send texts over %v characters", maxTextLen) + } + return costPerChar * float64(len(message)), nil +} + +func test(msgToCustomer, msgToSpouse string) { + defer fmt.Println("========") + fmt.Println("Message for customer:", msgToCustomer) + fmt.Println("Message for spouse:", msgToSpouse) + totalCost, err := sendSMSToCouple(msgToCustomer, msgToSpouse) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Printf("Total cost: $%.4f\n", totalCost) +} + +func main() { + test( + "Thanks for coming in to our flower shop today!", + "We hope you enjoyed your gift.", + ) + test( + "Thanks for joining us!", + "Have a good day.", + ) + test( + "Thank you.", + "Enjoy!", + ) + test( + "We loved having you in!", + "We hope the rest of your evening is absolutely fantastic.", + ) +} diff --git a/course/6-errors/exercises/1-errors/expected.txt b/course/6-errors/exercises/1-errors/expected.txt new file mode 100644 index 0000000..027c1da --- /dev/null +++ b/course/6-errors/exercises/1-errors/expected.txt @@ -0,0 +1,16 @@ +Message for customer: Thanks for coming in to our flower shop today! +Message for spouse: We hope you enjoyed your gift. +Error: can't send texts over 25 characters +======== +Message for customer: Thanks for joining us! +Message for spouse: Have a good day. +Total cost: $0.0076 +======== +Message for customer: Thank you. +Message for spouse: Enjoy! +Total cost: $0.0032 +======== +Message for customer: We loved having you in! +Message for spouse: We hope the rest of your evening is absolutely fantastic. +Error: can't send texts over 25 characters +======== diff --git a/course/6-errors/exercises/1-errors/readme.md b/course/6-errors/exercises/1-errors/readme.md new file mode 100644 index 0000000..65978bf --- /dev/null +++ b/course/6-errors/exercises/1-errors/readme.md @@ -0,0 +1,40 @@ +# The Error Interface + +Go programs express errors with `error` values. An Error is any type that implements the simple built-in [error interface](https://blog.golang.org/error-handling-and-go): + +```go +type error interface { + Error() string +} +``` + +When something can go wrong in a function, that function should return an `error` as its last return value. Any code that calls a function that can return an `error` should handle errors by testing whether the error is `nil`. + +```go +// Atoi converts a stringified number to an interger +i, err := strconv.Atoi("42b") +if err != nil { + fmt.Println("couldn't convert:", err) + // because "42b" isn't a valid integer, we print: + // couldn't convert: strconv.Atoi: parsing "42b": invalid syntax + // Note: + // 'parsing "42b": invalid syntax' is returned by the .Error() method + return +} +// if we get here, then +// i was converted successfully +``` + +A `nil` error denotes success; a non-nil error denotes failure. + +## Assignment + +We offer a product that allows businesses that use Textio to send pairs of messages to couples. It is mostly used by flower shops and movie theaters. + +Complete the `sendSMSToCouple` function. It should send 2 messages, first to the customer, then to the customer's spouse. + +1. Use `sendSMS()` to send the `msgToCustomer`. If an error is encountered, return `0.0` and the error. +2. Do the same for the `msgToSpouse` +3. If both messages are sent successfully, return the total cost of the messages added together. + +*When you return a non-nil error in Go, it's conventional to return the "zero" values of all other return values.* diff --git a/course/6-errors/exercises/2-formatting_strings/code.go b/course/6-errors/exercises/2-formatting_strings/code.go new file mode 100644 index 0000000..30116a4 --- /dev/null +++ b/course/6-errors/exercises/2-formatting_strings/code.go @@ -0,0 +1,20 @@ +package main + +func getSMSErrorString(cost float64, recipient string) string { + // ? +} + +// don't edit below this line + +func test(cost float64, recipient string) { + s := getSMSErrorString(cost, recipient) + fmt.Println(s) + fmt.Println("====================================") +} + +func main() { + test(1.4, "+1 (435) 555 0923") + test(2.1, "+2 (702) 555 3452") + test(32.1, "+1 (801) 555 7456") + test(14.4, "+1 (234) 555 6545") +} diff --git a/course/6-errors/exercises/2-formatting_strings/complete.go b/course/6-errors/exercises/2-formatting_strings/complete.go new file mode 100644 index 0000000..6a4ec5f --- /dev/null +++ b/course/6-errors/exercises/2-formatting_strings/complete.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" +) + +func getSMSErrorString(cost float64, recipient string) string { + return fmt.Sprintf("SMS that costs $%.2f to be sent to '%v' can not be sent", + cost, + recipient, + ) +} + +// don't edit below this line + +func test(cost float64, recipient string) { + s := getSMSErrorString(cost, recipient) + fmt.Println(s) + fmt.Println("====================================") +} + +func main() { + test(1.4, "+1 (435) 555 0923") + test(2.1, "+2 (702) 555 3452") + test(32.1, "+1 (801) 555 7456") + test(14.4, "+1 (234) 555 6545") +} diff --git a/course/6-errors/exercises/2-formatting_strings/expected.txt b/course/6-errors/exercises/2-formatting_strings/expected.txt new file mode 100644 index 0000000..e934f9a --- /dev/null +++ b/course/6-errors/exercises/2-formatting_strings/expected.txt @@ -0,0 +1,8 @@ +SMS that costs $1.40 to be sent to '+1 (435) 555 0923' can not be sent +==================================== +SMS that costs $2.10 to be sent to '+2 (702) 555 3452' can not be sent +==================================== +SMS that costs $32.10 to be sent to '+1 (801) 555 7456' can not be sent +==================================== +SMS that costs $14.40 to be sent to '+1 (234) 555 6545' can not be sent +==================================== diff --git a/course/6-errors/exercises/2-formatting_strings/readme.md b/course/6-errors/exercises/2-formatting_strings/readme.md new file mode 100644 index 0000000..ededa0a --- /dev/null +++ b/course/6-errors/exercises/2-formatting_strings/readme.md @@ -0,0 +1,47 @@ +# Formatting strings review + +A convenient way to format strings in Go is by using the standard library's [fmt.Sprintf()](https://pkg.go.dev/fmt#example-Sprintf) function. It's a string interpolation function, similar to JavaScript's built-in template literals. The `%v` substring uses the type's default formatting, which is often what you want. + +### Default values + +```go +const name = "Kim" +const age = 22 +s := fmt.Sprintf("%v is %v years old.", name, age) +// s = "Kim is 22 years old." +``` + +The equivalent JavaScript code: + +```js +const name = 'Kim' +const age = 22 +s = `${name} is ${age} years old.` +// s = "Kim is 22 years old." +``` + +### Rounding floats + +```go +fmt.Printf("I am %f years old", 10.523) +// I am 10.523000 years old + +// The ".2" rounds the number to 2 decimal places +fmt.Printf("I am %.2f years old", 10.523) +// I am 10.53 years old +``` + +## Assignment + +We need better error logs for our backend developers to help them debug their code. + +Complete the `getSMSErrorString()` function. It should return a string with this format: + +``` +SMS that costs $COST to be sent to 'RECIPIENT' can not be sent +``` + +* `COST` is the cost of the SMS, always showing the price formatted to 2 decimal places. +* `RECIPIENT` is the stringified representation of the recipient's phone number + +*Be sure to include the $ symbol and the single quotes* diff --git a/course/6-errors/exercises/3-custom_errors/code.go b/course/6-errors/exercises/3-custom_errors/code.go new file mode 100644 index 0000000..dad782d --- /dev/null +++ b/course/6-errors/exercises/3-custom_errors/code.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" +) + +type divideError struct { + dividend float64 +} + +// ? + +// don't edit below this line + +func divide(dividend, divisor float64) (float64, error) { + if divisor == 0 { + // We convert the `divideError` struct to an `error` type by returning it + // as an error. As an error type, when it's printed its default value + // will be the result of the Error() method + return 0, divideError{dividend: dividend} + } + return dividend / divisor, nil +} + +func test(dividend, divisor float64) { + defer fmt.Println("====================================") + fmt.Printf("Dividing %.2f by %.2f ...\n", dividend, divisor) + quotient, err := divide(dividend, divisor) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Quotient: %.2f\n", quotient) +} + +func main() { + test(10, 0) + test(10, 2) + test(15, 30) + test(6, 3) +} diff --git a/course/6-errors/exercises/3-custom_errors/complete.go b/course/6-errors/exercises/3-custom_errors/complete.go new file mode 100644 index 0000000..3d33134 --- /dev/null +++ b/course/6-errors/exercises/3-custom_errors/complete.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" +) + +type divideError struct { + dividend float64 +} + +func (de divideError) Error() string { + return fmt.Sprintf("can not divide %v by zero", de.dividend) +} + +// don't edit below this line + +func divide(dividend, divisor float64) (float64, error) { + if divisor == 0 { + // We convert the `divideError` struct to an `error` type by returning it + // as an error. As an error type, when it's printed its default value + // will be the result of the Error() method + return 0, divideError{dividend: dividend} + } + return dividend / divisor, nil +} + +func test(dividend, divisor float64) { + defer fmt.Println("====================================") + fmt.Printf("Dividing %.2f by %.2f ...\n", dividend, divisor) + quotient, err := divide(dividend, divisor) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Quotient: %.2f\n", quotient) +} + +func main() { + test(10, 0) + test(10, 2) + test(15, 30) + test(6, 3) +} diff --git a/course/6-errors/exercises/3-custom_errors/expected.txt b/course/6-errors/exercises/3-custom_errors/expected.txt new file mode 100644 index 0000000..1d4e9c0 --- /dev/null +++ b/course/6-errors/exercises/3-custom_errors/expected.txt @@ -0,0 +1,12 @@ +Dividing 10.00 by 0.00 ... +can not divide 10 by zero +==================================== +Dividing 10.00 by 2.00 ... +Quotient: 5.00 +==================================== +Dividing 15.00 by 30.00 ... +Quotient: 0.50 +==================================== +Dividing 6.00 by 3.00 ... +Quotient: 2.00 +==================================== diff --git a/course/6-errors/exercises/3-custom_errors/readme.md b/course/6-errors/exercises/3-custom_errors/readme.md new file mode 100644 index 0000000..6bac5b1 --- /dev/null +++ b/course/6-errors/exercises/3-custom_errors/readme.md @@ -0,0 +1,36 @@ +# The Error Interface + +Because errors are just interfaces, you can build your own custom types that implement the `error` interface. Here's an example of a `userError` struct that implements the `error` interface: + +```go +type userError struct { + name string +} + +func (e userError) Error() string { + return fmt.Sprintf("%v has a problem with their account", e.name) +} +``` + +It can then be used as an error: + +```go +func sendSMS(msg, userName string) error { + if !canSendToUser(userName) { + return userError{name: userName} + } + ... +} +``` + +## Assignment + +Our users are frequently trying to run custom analytics queries on their message deliverability metrics, and end up writing a bad query that tries to divide a number by zero. It's become such a problem, that we think it would be best to make a specific type of error for division by zero. + +Update the code so that the `divideError` type implements the `error` interface. Its `Error()` method should just return a string formatted in the following way: + +``` +can not divide DIVIDEND by zero +``` + +Where `DIVIDEND` is the actual dividend of the `divideError`. Use the `%v` [verb](https://pkg.go.dev/fmt#hdr-Printing) to format the dividend as a float. diff --git a/course/6-errors/exercises/4a-errors_quiz/multiple_choice.json b/course/6-errors/exercises/4a-errors_quiz/multiple_choice.json new file mode 100644 index 0000000..ad7a394 --- /dev/null +++ b/course/6-errors/exercises/4a-errors_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What is the underlying type of an error?", + "answers": [ + "Interface", + "Struct", + "String" + ] +} diff --git a/course/6-errors/exercises/4a-errors_quiz/readme.md b/course/6-errors/exercises/4a-errors_quiz/readme.md new file mode 100644 index 0000000..465229b --- /dev/null +++ b/course/6-errors/exercises/4a-errors_quiz/readme.md @@ -0,0 +1,5 @@ +# Errors Quiz + +Go programs express errors with `error` values. Error-values are any type that implements the simple built-in [error interface](https://blog.golang.org/error-handling-and-go). + +Keep in mind that the way Go handles errors is fairly unique. Most languages treat errors as something special and different. For example, Python raises exception types and JavaScript throws and catches errors. In Go, an `error` is just another value that we handle like any other value - however, we want! There aren't any special keywords for dealing with them. diff --git a/course/6-errors/exercises/4b-errors_quiz/multiple_choice.json b/course/6-errors/exercises/4b-errors_quiz/multiple_choice.json new file mode 100644 index 0000000..307ab7a --- /dev/null +++ b/course/6-errors/exercises/4b-errors_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Can a type be an error and also fulfill another interface?", + "answers": [ + "Yes", + "No" + ] +} diff --git a/course/6-errors/exercises/4b-errors_quiz/readme.md b/course/6-errors/exercises/4b-errors_quiz/readme.md new file mode 100644 index 0000000..465229b --- /dev/null +++ b/course/6-errors/exercises/4b-errors_quiz/readme.md @@ -0,0 +1,5 @@ +# Errors Quiz + +Go programs express errors with `error` values. Error-values are any type that implements the simple built-in [error interface](https://blog.golang.org/error-handling-and-go). + +Keep in mind that the way Go handles errors is fairly unique. Most languages treat errors as something special and different. For example, Python raises exception types and JavaScript throws and catches errors. In Go, an `error` is just another value that we handle like any other value - however, we want! There aren't any special keywords for dealing with them. diff --git a/course/6-errors/exercises/5-errors_package/code.go b/course/6-errors/exercises/5-errors_package/code.go new file mode 100644 index 0000000..d30cd21 --- /dev/null +++ b/course/6-errors/exercises/5-errors_package/code.go @@ -0,0 +1,33 @@ +package main + +import ( + "errors" + "fmt" +) + +func divide(x, y float64) (float64, error) { + if y == 0 { + // ? + } + return x / y, nil +} + +// don't edit below this line + +func test(x, y float64) { + defer fmt.Println("====================================") + fmt.Printf("Dividing %.2f by %.2f ...\n", x, y) + quotient, err := divide(x, y) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Quotient: %.2f\n", quotient) +} + +func main() { + test(10, 0) + test(10, 2) + test(15, 30) + test(6, 3) +} diff --git a/course/6-errors/exercises/5-errors_package/complete.go b/course/6-errors/exercises/5-errors_package/complete.go new file mode 100644 index 0000000..e097e5e --- /dev/null +++ b/course/6-errors/exercises/5-errors_package/complete.go @@ -0,0 +1,33 @@ +package main + +import ( + "errors" + "fmt" +) + +func divide(x, y float64) (float64, error) { + if y == 0 { + return 0, errors.New("no dividing by 0") + } + return x / y, nil +} + +// don't edit below this line + +func test(x, y float64) { + defer fmt.Println("====================================") + fmt.Printf("Dividing %.2f by %.2f ...\n", x, y) + quotient, err := divide(x, y) + if err != nil { + fmt.Println(err) + return + } + fmt.Printf("Quotient: %.2f\n", quotient) +} + +func main() { + test(10, 0) + test(10, 2) + test(15, 30) + test(6, 3) +} diff --git a/course/6-errors/exercises/5-errors_package/expected.txt b/course/6-errors/exercises/5-errors_package/expected.txt new file mode 100644 index 0000000..389ec36 --- /dev/null +++ b/course/6-errors/exercises/5-errors_package/expected.txt @@ -0,0 +1,12 @@ +Dividing 10.00 by 0.00 ... +no dividing by 0 +==================================== +Dividing 10.00 by 2.00 ... +Quotient: 5.00 +==================================== +Dividing 15.00 by 30.00 ... +Quotient: 0.50 +==================================== +Dividing 6.00 by 3.00 ... +Quotient: 2.00 +==================================== diff --git a/course/6-errors/exercises/5-errors_package/readme.md b/course/6-errors/exercises/5-errors_package/readme.md new file mode 100644 index 0000000..4a49c27 --- /dev/null +++ b/course/6-errors/exercises/5-errors_package/readme.md @@ -0,0 +1,15 @@ +# The Errors Package + +The Go standard library provides an "errors" package that makes it easy to deal with errors. + +Read the godoc for the [errors.New()](https://pkg.go.dev/errors#New) function, but here's a simple example: + +```go +var err error = errors.New("something went wrong") +``` + +## Assignment + +Twilio's software architects may have overcomplicated the requirements from the last coding assignment... oops. All we needed was a new generic error message that returns the string `no dividing by 0` when a user attempts to get us to perform the taboo. + +Complete the `divide` function. Use the `errors.New()` function to return an error when `y == 0` that reads "no dividing by 0". diff --git a/course/7-loops/exercises/1-intro/code.go b/course/7-loops/exercises/1-intro/code.go new file mode 100644 index 0000000..3629140 --- /dev/null +++ b/course/7-loops/exercises/1-intro/code.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" +) + +func bulkSend(numMessages int) float64 { + // ? +} + +// don't edit below this line + +func test(numMessages int) { + fmt.Printf("Sending %v messages\n", numMessages) + cost := bulkSend(numMessages) + fmt.Printf("Bulk send complete! Cost = %.2f\n", cost) + fmt.Println("===============================================================") +} + +func main() { + test(10) + test(20) + test(30) + test(40) + test(50) +} diff --git a/course/7-loops/exercises/1-intro/complete.go b/course/7-loops/exercises/1-intro/complete.go new file mode 100644 index 0000000..9714def --- /dev/null +++ b/course/7-loops/exercises/1-intro/complete.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" +) + +func bulkSend(numMessages int) float64 { + totalCost := 0.0 + for i := 0; i < numMessages; i++ { + totalCost += 1 + (float64(i) * 0.01) + } + return totalCost +} + +// don't edit below this line + +func test(numMessages int) { + fmt.Printf("Sending %v messages\n", numMessages) + cost := bulkSend(numMessages) + fmt.Printf("Bulk send complete! Cost = %.2f\n", cost) + fmt.Println("===============================================================") +} + +func main() { + test(10) + test(20) + test(30) + test(40) + test(50) +} diff --git a/course/7-loops/exercises/1-intro/expected.txt b/course/7-loops/exercises/1-intro/expected.txt new file mode 100644 index 0000000..d860248 --- /dev/null +++ b/course/7-loops/exercises/1-intro/expected.txt @@ -0,0 +1,15 @@ +Sending 10 messages +Bulk send complete! Cost = 10.45 +=============================================================== +Sending 20 messages +Bulk send complete! Cost = 21.90 +=============================================================== +Sending 30 messages +Bulk send complete! Cost = 34.35 +=============================================================== +Sending 40 messages +Bulk send complete! Cost = 47.80 +=============================================================== +Sending 50 messages +Bulk send complete! Cost = 62.25 +=============================================================== diff --git a/course/7-loops/exercises/1-intro/readme.md b/course/7-loops/exercises/1-intro/readme.md new file mode 100644 index 0000000..1f53657 --- /dev/null +++ b/course/7-loops/exercises/1-intro/readme.md @@ -0,0 +1,41 @@ +# Loops in Go + +The basic loop in Go is written in standard C-like syntax: + +```go +for INITIAL; CONDITION; AFTER{ + // do something +} +``` + +`INITIAL` is run once at the beginning of the loop and can create +variables within the scope of the loop. + +`CONDITION` is checked before each iteration. If the condition doesn't pass +then the loop breaks. + +`AFTER` is run after each iteration. + +For example: + +```go +for i := 0; i < 10; i++ { + fmt.Println(i) +} +// Prints 0 through 9 +``` + +## Assignment + +At Textio we have a dynamic formula for determining how much a batch of bulk messages costs to send. + +### Complete the `bulkSend()` function + +This function should return the total cost (as a `float64`) to send a batch of `numMessages` messages. Each message costs `1.0`, plus an additional fee. The fee structure is: + +* 1st message: `1.0 + 0.00` +* 2nd message: `1.0 + 0.01` +* 3rd message: `1.0 + 0.02` +* 4th message: `1.0 + 0.03` + +Use a loop to calculate the total cost and return it. diff --git a/course/7-loops/exercises/2-omit_condition/code.go b/course/7-loops/exercises/2-omit_condition/code.go new file mode 100644 index 0000000..83ff1cb --- /dev/null +++ b/course/7-loops/exercises/2-omit_condition/code.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" +) + +func maxMessages(thresh float64) int { + // ? +} + +// don't edit below this line + +func test(thresh float64) { + fmt.Printf("Threshold: %.2f\n", thresh) + max := maxMessages(thresh) + fmt.Printf("Maximum messages that can be sent: = %v\n", max) + fmt.Println("===============================================================") +} + +func main() { + test(10.00) + test(20.00) + test(30.00) + test(40.00) + test(50.00) +} diff --git a/course/7-loops/exercises/2-omit_condition/complete.go b/course/7-loops/exercises/2-omit_condition/complete.go new file mode 100644 index 0000000..0514cec --- /dev/null +++ b/course/7-loops/exercises/2-omit_condition/complete.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" +) + +func maxMessages(thresh float64) int { + totalCost := 0.0 + for i := 0; ; i++ { + totalCost += 1 + (float64(i) * 0.01) + if totalCost > thresh { + return i + } + } +} + +// don't edit below this line + +func test(thresh float64) { + fmt.Printf("Threshold: %.2f\n", thresh) + max := maxMessages(thresh) + fmt.Printf("Maximum messages that can be sent: = %v\n", max) + fmt.Println("===============================================================") +} + +func main() { + test(10.00) + test(20.00) + test(30.00) + test(40.00) + test(50.00) +} diff --git a/course/7-loops/exercises/2-omit_condition/expected.txt b/course/7-loops/exercises/2-omit_condition/expected.txt new file mode 100644 index 0000000..5cc3327 --- /dev/null +++ b/course/7-loops/exercises/2-omit_condition/expected.txt @@ -0,0 +1,15 @@ +Threshold: 10.00 +Maximum messages that can be sent: = 9 +=============================================================== +Threshold: 20.00 +Maximum messages that can be sent: = 18 +=============================================================== +Threshold: 30.00 +Maximum messages that can be sent: = 26 +=============================================================== +Threshold: 40.00 +Maximum messages that can be sent: = 34 +=============================================================== +Threshold: 50.00 +Maximum messages that can be sent: = 41 +=============================================================== diff --git a/course/7-loops/exercises/2-omit_condition/readme.md b/course/7-loops/exercises/2-omit_condition/readme.md new file mode 100644 index 0000000..2cbe7c1 --- /dev/null +++ b/course/7-loops/exercises/2-omit_condition/readme.md @@ -0,0 +1,24 @@ +# Omitting conditions from a for loop in Go + +Loops in Go can omit sections of a for loop. For example, the `CONDITION` (middle part) can be omitted which causes the loop to run forever. + +```go +for INITIAL; ; AFTER { + // do something forever +} +``` + +## Assignment + +Complete the `maxMessages` function. Given a cost threshold, it should calculate the maximum number of messages that can be sent. + +Each message costs `1.0`, plus an additional fee. The fee structure is: + +* 1st message: `1.0 + 0.00` +* 2nd message: `1.0 + 0.01` +* 3rd message: `1.0 + 0.02` +* 4th message: `1.0 + 0.03` + +## Browser freeze + +If you lock up your browser by creating an infinite loop that isn't breaking, just click the `cancel` button. diff --git a/course/7-loops/exercises/3-while/code.go b/course/7-loops/exercises/3-while/code.go new file mode 100644 index 0000000..63d3ba1 --- /dev/null +++ b/course/7-loops/exercises/3-while/code.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" +) + +func getMaxMessagesToSend(costMultiplier float64, maxCostInPennies int) int { + actualCostInPennies := 1.0 + maxMessagesToSend := 0 + for { + maxMessagesToSend++ + actualCostInPennies *= costMultiplier + } + return maxMessagesToSend +} + +// don't touch below this line + +func test(costMultiplier float64, maxCostInPennies int) { + maxMessagesToSend := getMaxMessagesToSend(costMultiplier, maxCostInPennies) + fmt.Printf("Multiplier: %v\n", + costMultiplier, + ) + fmt.Printf("Max cost: %v\n", + maxCostInPennies, + ) + fmt.Printf("Max messages you can send: %v\n", + maxMessagesToSend, + ) + fmt.Println("====================================") +} + +func main() { + test(1.1, 5) + test(1.3, 10) + test(1.35, 25) +} diff --git a/course/7-loops/exercises/3-while/complete.go b/course/7-loops/exercises/3-while/complete.go new file mode 100644 index 0000000..b9e9d56 --- /dev/null +++ b/course/7-loops/exercises/3-while/complete.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" +) + +func getMaxMessagesToSend(costMultiplier float64, maxCostInPennies int) int { + actualCostInPennies := 1.0 + maxMessagesToSend := 0 + for actualCostInPennies <= float64(maxCostInPennies) { + maxMessagesToSend++ + actualCostInPennies *= costMultiplier + } + return maxMessagesToSend +} + +// don't touch below this line + +func test(costMultiplier float64, maxCostInPennies int) { + maxMessagesToSend := getMaxMessagesToSend(costMultiplier, maxCostInPennies) + fmt.Printf("Multiplier: %v\n", + costMultiplier, + ) + fmt.Printf("Max cost: %v\n", + maxCostInPennies, + ) + fmt.Printf("Max messages you can send: %v\n", + maxMessagesToSend, + ) + fmt.Println("====================================") +} + +func main() { + test(1.1, 5) + test(1.3, 10) + test(1.35, 25) +} diff --git a/course/7-loops/exercises/3-while/expected.txt b/course/7-loops/exercises/3-while/expected.txt new file mode 100644 index 0000000..ab4ff76 --- /dev/null +++ b/course/7-loops/exercises/3-while/expected.txt @@ -0,0 +1,12 @@ +Multiplier: 1.1 +Max cost: 5 +Max messages you can send: 17 +==================================== +Multiplier: 1.3 +Max cost: 10 +Max messages you can send: 9 +==================================== +Multiplier: 1.35 +Max cost: 25 +Max messages you can send: 11 +==================================== diff --git a/course/7-loops/exercises/3-while/readme.md b/course/7-loops/exercises/3-while/readme.md new file mode 100644 index 0000000..81192ef --- /dev/null +++ b/course/7-loops/exercises/3-while/readme.md @@ -0,0 +1,38 @@ +# There is no while loop in Go + +Most programming languages have a concept of a `while` loop. Because Go allows for the omission of sections of a `for` loop, a `while` loop is just a `for` loop that only has a CONDITION. + +```go +for CONDITION { + // do some stuff while CONDITION is true +} +``` + +For example: + +```go +plantHeight := 1 +for plantHeight < 5 { + fmt.Println("still growing! current height:", plantHeight) + plantHeight++ +} +fmt.Println("plant has grown to ", plantHeight, "inches") +``` + +Which prints: + +``` +still growing! current height: 1 +still growing! current height: 2 +still growing! current height: 3 +still growing! current height: 4 +plant has grown to 5 inches +``` + +## Assignment + +We have an interesting new cost structure from our SMS vendor. They charge exponentially more money for each consecutive text we send! Let's write a function that can calculate how many messages we can send in a given batch given a `costMultiplier` and a `maxCostInPennies`. + +In a nutshell, the first message costs a penny, and each message after that costs the same as the previous message multiplied by the `costMultiplier`. That gets expensive! + +There is an infinite loop in the code! Let's add a condition to fix the bug. The loop should exit **before** incrementing `maxMessagesToSend` if the cost of the next message would go over the max cost. diff --git a/course/7-loops/exercises/4-loops_fizzbuzz/code.go b/course/7-loops/exercises/4-loops_fizzbuzz/code.go new file mode 100644 index 0000000..82b1ec8 --- /dev/null +++ b/course/7-loops/exercises/4-loops_fizzbuzz/code.go @@ -0,0 +1,11 @@ +package main + +func fizzbuzz() { + // ? +} + +// don't touch below this line + +func main() { + fizzbuzz() +} diff --git a/course/7-loops/exercises/4-loops_fizzbuzz/complete.go b/course/7-loops/exercises/4-loops_fizzbuzz/complete.go new file mode 100644 index 0000000..06d54a1 --- /dev/null +++ b/course/7-loops/exercises/4-loops_fizzbuzz/complete.go @@ -0,0 +1,23 @@ +package main + +import "fmt" + +func fizzbuzz() { + for i := 1; i < 101; i++ { + if i%3 == 0 && i%5 == 0 { + fmt.Println("fizzbuzz") + } else if i%3 == 0 { + fmt.Println("fizz") + } else if i%5 == 0 { + fmt.Println("buzz") + } else { + fmt.Println(i) + } + } +} + +// don't touch below this line + +func main() { + fizzbuzz() +} diff --git a/course/7-loops/exercises/4-loops_fizzbuzz/expected.txt b/course/7-loops/exercises/4-loops_fizzbuzz/expected.txt new file mode 100644 index 0000000..27e9a38 --- /dev/null +++ b/course/7-loops/exercises/4-loops_fizzbuzz/expected.txt @@ -0,0 +1,100 @@ +1 +2 +fizz +4 +buzz +fizz +7 +8 +fizz +buzz +11 +fizz +13 +14 +fizzbuzz +16 +17 +fizz +19 +buzz +fizz +22 +23 +fizz +buzz +26 +fizz +28 +29 +fizzbuzz +31 +32 +fizz +34 +buzz +fizz +37 +38 +fizz +buzz +41 +fizz +43 +44 +fizzbuzz +46 +47 +fizz +49 +buzz +fizz +52 +53 +fizz +buzz +56 +fizz +58 +59 +fizzbuzz +61 +62 +fizz +64 +buzz +fizz +67 +68 +fizz +buzz +71 +fizz +73 +74 +fizzbuzz +76 +77 +fizz +79 +buzz +fizz +82 +83 +fizz +buzz +86 +fizz +88 +89 +fizzbuzz +91 +92 +fizz +94 +buzz +fizz +97 +98 +fizz +buzz diff --git a/course/7-loops/exercises/4-loops_fizzbuzz/readme.md b/course/7-loops/exercises/4-loops_fizzbuzz/readme.md new file mode 100644 index 0000000..d2011bb --- /dev/null +++ b/course/7-loops/exercises/4-loops_fizzbuzz/readme.md @@ -0,0 +1,27 @@ +# Fizzbuzz + +Go supports the standard [modulo operator](https://en.wikipedia.org/wiki/Modulo_operation): + +```go +7 % 3 // 1 +``` + +Logical [AND operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_AND): + +```go +true && false // false +true && true // true +``` + +Logical [OR operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_OR): + +```go +true || false // true +false || false // false +``` + +## Assignment + +We're hiring engineers at Textio, so time to brush up on the classic "Fizzbuzz" game, a coding exercise that has been dramatically overused in coding interviews across the world. + +Complete the `fizzbuzz` function that prints the numbers 1 to 100 inclusive each on their own line, but substitutes multiples of 3 for the text `fizz` and multiples of 5 for `buzz`. For multiples of 3 AND 5 print instead `fizzbuzz`. diff --git a/course/7-loops/exercises/5-continue_and_break/code.go b/course/7-loops/exercises/5-continue_and_break/code.go new file mode 100644 index 0000000..d3d0ccb --- /dev/null +++ b/course/7-loops/exercises/5-continue_and_break/code.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" +) + +func printPrimes(max int) { + // ? +} + +// don't edit below this line + +func test(max int) { + fmt.Printf("Primes up to %v:\n", max) + printPrimes(max) + fmt.Println("===============================================================") +} + +func main() { + test(10) + test(20) + test(30) +} diff --git a/course/7-loops/exercises/5-continue_and_break/complete.go b/course/7-loops/exercises/5-continue_and_break/complete.go new file mode 100644 index 0000000..76e7131 --- /dev/null +++ b/course/7-loops/exercises/5-continue_and_break/complete.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" +) + +func printPrimes(max int) { + for n := 2; n <= max; n++ { + if n == 2 { + fmt.Println(n) + continue + } + + if n%2 == 0 { + continue + } + + isPrime := true + for i := 3; i*i <= n; i++ { + if n%i == 0 { + isPrime = false + break + } + } + if isPrime { + fmt.Println(n) + } + } +} + +// don't edit below this line + +func test(max int) { + fmt.Printf("Primes up to %v:\n", max) + printPrimes(max) + fmt.Println("===============================================================") +} + +func main() { + test(10) + test(20) + test(30) +} diff --git a/course/7-loops/exercises/5-continue_and_break/expected.txt b/course/7-loops/exercises/5-continue_and_break/expected.txt new file mode 100644 index 0000000..23395ec --- /dev/null +++ b/course/7-loops/exercises/5-continue_and_break/expected.txt @@ -0,0 +1,28 @@ +Primes up to 10: +2 +3 +5 +7 +=============================================================== +Primes up to 20: +2 +3 +5 +7 +11 +13 +17 +19 +=============================================================== +Primes up to 30: +2 +3 +5 +7 +11 +13 +17 +19 +23 +29 +=============================================================== diff --git a/course/7-loops/exercises/5-continue_and_break/readme.md b/course/7-loops/exercises/5-continue_and_break/readme.md new file mode 100644 index 0000000..4a0810f --- /dev/null +++ b/course/7-loops/exercises/5-continue_and_break/readme.md @@ -0,0 +1,64 @@ +# Continue + +## `continue` + +The `continue` keyword stops the current iteration of a loop and continues to the next iteration. `continue` is a powerful way to use the "guard clause" pattern within loops. + +```go +for i := 0; i < 10; i++ { + if i % 2 == 0 { + continue + } + fmt.Println(i) +} +// 1 +// 3 +// 5 +// 7 +// 9 +``` + +## `break` + +The `break` keyword stops the current iteration of a loop and exits the loop. + +```go +for i := 0; i < 10; i++ { + if i == 5 { + break + } + fmt.Println(i) +} +// 0 +// 1 +// 2 +// 3 +// 4 +``` + +## Assignment + +As an easter egg, we decided to reward our users with a free text message if they send a [prime number](https://en.wikipedia.org/wiki/Prime_number) of text messages this year. + +Complete the `printPrimes` function. It should print all of the prime numbers up to and including `max`. It should skip any numbers that are not prime. + +Here's the psuedocode: + +``` +printPrimes(max): + for n in range(2, max+1): + if n is 2: + n is prime, print it + if n is even: + n is not prime, skip to next n + for i in range (3, sqrt(n) + 1): + if i can be multiplied into n: + n is not prime, skip to next n + n is prime, print it +``` + +### Breakdown + +* We skip even numbers because they can't be prime +* We only check up to the square root because anything higher than the square root has no chance of multiplying evenly into `n` +* We start checking at 2 because 1 is not prime diff --git a/course/8-slices/exercises/1-arrays/code.go b/course/8-slices/exercises/1-arrays/code.go new file mode 100644 index 0000000..e905f44 --- /dev/null +++ b/course/8-slices/exercises/1-arrays/code.go @@ -0,0 +1,41 @@ +package main + +import "fmt" + +const ( + retry1 = "click here to sign up" + retry2 = "pretty please click here" + retry3 = "we beg you to sign up" +) + +func getMessageWithRetries() [3]string { + // ? +} + +// don't touch below this line + +func testSend(name string, doneAt int) { + fmt.Printf("sending to %v...", name) + fmt.Println() + + messages := getMessageWithRetries() + for i := 0; i < len(messages); i++ { + msg := messages[i] + fmt.Printf(`sending: "%v"`, msg) + fmt.Println() + if i == doneAt { + fmt.Println("they responded!") + break + } + if i == len(messages)-1 { + fmt.Println("complete failure") + } + } +} + +func main() { + testSend("Bob", 0) + testSend("Alice", 1) + testSend("Mangalam", 2) + testSend("Ozgur", 3) +} diff --git a/course/8-slices/exercises/1-arrays/complete.go b/course/8-slices/exercises/1-arrays/complete.go new file mode 100644 index 0000000..5fe34ff --- /dev/null +++ b/course/8-slices/exercises/1-arrays/complete.go @@ -0,0 +1,45 @@ +package main + +import "fmt" + +const ( + retry1 = "click here to sign up" + retry2 = "pretty please click here" + retry3 = "we beg you to sign up" +) + +func getMessageWithRetries() [3]string { + return [3]string{ + retry1, + retry2, + retry3, + } +} + +// don't touch below this line + +func send(name string, doneAt int) { + fmt.Printf("sending to %v...", name) + fmt.Println() + + messages := getMessageWithRetries() + for i := 0; i < len(messages); i++ { + msg := messages[i] + fmt.Printf(`sending: "%v"`, msg) + fmt.Println() + if i == doneAt { + fmt.Println("they responded!") + break + } + if i == len(messages)-1 { + fmt.Println("complete failure") + } + } +} + +func main() { + send("Bob", 0) + send("Alice", 1) + send("Mangalam", 2) + send("Ozgur", 3) +} diff --git a/course/8-slices/exercises/1-arrays/expected.txt b/course/8-slices/exercises/1-arrays/expected.txt new file mode 100644 index 0000000..5d689f5 --- /dev/null +++ b/course/8-slices/exercises/1-arrays/expected.txt @@ -0,0 +1,17 @@ +sending to Bob... +sending: "click here to sign up" +they responded! +sending to Alice... +sending: "click here to sign up" +sending: "pretty please click here" +they responded! +sending to Mangalam... +sending: "click here to sign up" +sending: "pretty please click here" +sending: "we beg you to sign up" +they responded! +sending to Ozgur... +sending: "click here to sign up" +sending: "pretty please click here" +sending: "we beg you to sign up" +complete failure diff --git a/course/8-slices/exercises/1-arrays/readme.md b/course/8-slices/exercises/1-arrays/readme.md new file mode 100644 index 0000000..7546393 --- /dev/null +++ b/course/8-slices/exercises/1-arrays/readme.md @@ -0,0 +1,29 @@ +# Arrays in Go + +Arrays are fixed-size groups of variables of the same type. + +The type `[n]T` is an array of n values of type `T` + +To declare an array of 10 integers: + +```go +var myInts [10]int +``` + +or to declare an initialized literal: + +```go +primes := [6]int{2, 3, 5, 7, 11, 13} +``` + +## Assignment + +When a message isn't responded to, we allow our clients to have up to 2 additional messages that are sent as nudging reminders. + +`getMessageWithRetries` returns an array of 3 strings where index `0` is the first message. If the first message isn't answered by the recipient, we send the second, if that one isn't answered then we send the third. + +Update `getMessageWithRetries` to return the following 3 strings in an array. + +* `click here to sign up` +* `pretty please click here` +* `we beg you to sign up` diff --git a/course/8-slices/exercises/10-slice_gotcha/multiple_choice.json b/course/8-slices/exercises/10-slice_gotcha/multiple_choice.json new file mode 100644 index 0000000..d06a262 --- /dev/null +++ b/course/8-slices/exercises/10-slice_gotcha/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Why is 5 the final value in the last index of array 'j'?", + "answers": [ + "j and g point to the same underlying array so g's append overwrote j", + "The Go team be trollin'", + "Because append only works properly when the number of elements is < 10" + ] +} diff --git a/course/8-slices/exercises/10-slice_gotcha/readme.md b/course/8-slices/exercises/10-slice_gotcha/readme.md new file mode 100644 index 0000000..b73c017 --- /dev/null +++ b/course/8-slices/exercises/10-slice_gotcha/readme.md @@ -0,0 +1,77 @@ +# Tricky Slices + +The `append()` function changes the underlying array of its parameter AND returns a new slice. This means that using `append()` on anything other than itself is usually a BAD idea. + +```go +// dont do this! +someSlice = append(otherSlice, element) +``` + +Take a look at these head-scratchers: + +## Example 1: Works as expected + +```go +a := make([]int, 3) +fmt.Println("len of a:", len(a)) +// len of a: 3 +fmt.Println("cap of a:", cap(a)) +// cap of a: 3 +fmt.Println("appending 4 to b from a") +// appending 4 to b from a +b := append(a, 4) +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("addr of b:", &b[0]) +// addr of b: 0x44a0c0 +fmt.Println("appending 5 to c from a") +// appending 5 to c from a +c := append(a, 5) +fmt.Println("addr of c:", &c[0]) +// addr of c: 0x44a180 +fmt.Println("a:", a) +// a: [0 0 0] +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("c:", c) +// c: [0 0 0 5] +``` + +With slices `a`, `b`, and `c`, `4` and `5` seem to be appended as we would expect. We can even check the memory addresses and confirm that `b` and `c` point to different underlying arrays. + +## Example 2: Something fishy + +```go +i := make([]int, 3, 8) +fmt.Println("len of i:", len(i)) +// len of i: 3 +fmt.Println("cap of i:", cap(i)) +// cap of i: 8 +fmt.Println("appending 4 to j from i") +// appending 4 to j from i +j := append(i, 4) +fmt.Println("j:", j) +// j: [0 0 0 4] +fmt.Println("addr of j:", &j[0]) +// addr of j: 0x454000 +fmt.Println("appending 5 to g from i") +// appending 5 to g from i +g := append(i, 5) +fmt.Println("addr of g:", &g[0]) +// addr of g: 0x454000 +fmt.Println("i:", i) +// i: [0 0 0] +fmt.Println("j:", j) +// j: [0 0 0 5] +fmt.Println("g:", g) +// g: [0 0 0 5] +``` + +In this example however, when `5` is appended to `g` it overwrites `j`'s fourth index because `j` and `g` point to the *same underlying array*. The `append()` function only creates a new array when there isn't any capacity left. We created `i` with a length of 3 and a capactiy of 8, which means we can append `5` items before a new array is automatically allocated. + +Again, to avoid bugs like this, you should always use the `append` function on the same slice the result is assigned to: + +```go +mySlice := []int{1, 2, 3} +mySlice = append(mySlice, 4) +``` diff --git a/course/8-slices/exercises/10a-slice_gotcha/multiple_choice.json b/course/8-slices/exercises/10a-slice_gotcha/multiple_choice.json new file mode 100644 index 0000000..969d93c --- /dev/null +++ b/course/8-slices/exercises/10a-slice_gotcha/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Why doesn't the bug regarding slices 'j' and 'g' in example 2 occur in example 1 as well?", + "answers": [ + "The array's cap() is exceeded so a new underlying array is allocated", + "Because there are fewer elements and Go's runtime can't handle more than ~8" + ] +} diff --git a/course/8-slices/exercises/10a-slice_gotcha/readme.md b/course/8-slices/exercises/10a-slice_gotcha/readme.md new file mode 100644 index 0000000..b73c017 --- /dev/null +++ b/course/8-slices/exercises/10a-slice_gotcha/readme.md @@ -0,0 +1,77 @@ +# Tricky Slices + +The `append()` function changes the underlying array of its parameter AND returns a new slice. This means that using `append()` on anything other than itself is usually a BAD idea. + +```go +// dont do this! +someSlice = append(otherSlice, element) +``` + +Take a look at these head-scratchers: + +## Example 1: Works as expected + +```go +a := make([]int, 3) +fmt.Println("len of a:", len(a)) +// len of a: 3 +fmt.Println("cap of a:", cap(a)) +// cap of a: 3 +fmt.Println("appending 4 to b from a") +// appending 4 to b from a +b := append(a, 4) +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("addr of b:", &b[0]) +// addr of b: 0x44a0c0 +fmt.Println("appending 5 to c from a") +// appending 5 to c from a +c := append(a, 5) +fmt.Println("addr of c:", &c[0]) +// addr of c: 0x44a180 +fmt.Println("a:", a) +// a: [0 0 0] +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("c:", c) +// c: [0 0 0 5] +``` + +With slices `a`, `b`, and `c`, `4` and `5` seem to be appended as we would expect. We can even check the memory addresses and confirm that `b` and `c` point to different underlying arrays. + +## Example 2: Something fishy + +```go +i := make([]int, 3, 8) +fmt.Println("len of i:", len(i)) +// len of i: 3 +fmt.Println("cap of i:", cap(i)) +// cap of i: 8 +fmt.Println("appending 4 to j from i") +// appending 4 to j from i +j := append(i, 4) +fmt.Println("j:", j) +// j: [0 0 0 4] +fmt.Println("addr of j:", &j[0]) +// addr of j: 0x454000 +fmt.Println("appending 5 to g from i") +// appending 5 to g from i +g := append(i, 5) +fmt.Println("addr of g:", &g[0]) +// addr of g: 0x454000 +fmt.Println("i:", i) +// i: [0 0 0] +fmt.Println("j:", j) +// j: [0 0 0 5] +fmt.Println("g:", g) +// g: [0 0 0 5] +``` + +In this example however, when `5` is appended to `g` it overwrites `j`'s fourth index because `j` and `g` point to the *same underlying array*. The `append()` function only creates a new array when there isn't any capacity left. We created `i` with a length of 3 and a capactiy of 8, which means we can append `5` items before a new array is automatically allocated. + +Again, to avoid bugs like this, you should always use the `append` function on the same slice the result is assigned to: + +```go +mySlice := []int{1, 2, 3} +mySlice = append(mySlice, 4) +``` diff --git a/course/8-slices/exercises/10b-slice_gotcha/multiple_choice.json b/course/8-slices/exercises/10b-slice_gotcha/multiple_choice.json new file mode 100644 index 0000000..eb1c55b --- /dev/null +++ b/course/8-slices/exercises/10b-slice_gotcha/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "How can you best avoid these kinds of bugs?", + "answers": [ + "Always assign the result of the append() function back to the same slice", + "Always assign the result of the append() function to a new slice", + "Don't use the append() function" + ] +} diff --git a/course/8-slices/exercises/10b-slice_gotcha/readme.md b/course/8-slices/exercises/10b-slice_gotcha/readme.md new file mode 100644 index 0000000..b73c017 --- /dev/null +++ b/course/8-slices/exercises/10b-slice_gotcha/readme.md @@ -0,0 +1,77 @@ +# Tricky Slices + +The `append()` function changes the underlying array of its parameter AND returns a new slice. This means that using `append()` on anything other than itself is usually a BAD idea. + +```go +// dont do this! +someSlice = append(otherSlice, element) +``` + +Take a look at these head-scratchers: + +## Example 1: Works as expected + +```go +a := make([]int, 3) +fmt.Println("len of a:", len(a)) +// len of a: 3 +fmt.Println("cap of a:", cap(a)) +// cap of a: 3 +fmt.Println("appending 4 to b from a") +// appending 4 to b from a +b := append(a, 4) +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("addr of b:", &b[0]) +// addr of b: 0x44a0c0 +fmt.Println("appending 5 to c from a") +// appending 5 to c from a +c := append(a, 5) +fmt.Println("addr of c:", &c[0]) +// addr of c: 0x44a180 +fmt.Println("a:", a) +// a: [0 0 0] +fmt.Println("b:", b) +// b: [0 0 0 4] +fmt.Println("c:", c) +// c: [0 0 0 5] +``` + +With slices `a`, `b`, and `c`, `4` and `5` seem to be appended as we would expect. We can even check the memory addresses and confirm that `b` and `c` point to different underlying arrays. + +## Example 2: Something fishy + +```go +i := make([]int, 3, 8) +fmt.Println("len of i:", len(i)) +// len of i: 3 +fmt.Println("cap of i:", cap(i)) +// cap of i: 8 +fmt.Println("appending 4 to j from i") +// appending 4 to j from i +j := append(i, 4) +fmt.Println("j:", j) +// j: [0 0 0 4] +fmt.Println("addr of j:", &j[0]) +// addr of j: 0x454000 +fmt.Println("appending 5 to g from i") +// appending 5 to g from i +g := append(i, 5) +fmt.Println("addr of g:", &g[0]) +// addr of g: 0x454000 +fmt.Println("i:", i) +// i: [0 0 0] +fmt.Println("j:", j) +// j: [0 0 0 5] +fmt.Println("g:", g) +// g: [0 0 0 5] +``` + +In this example however, when `5` is appended to `g` it overwrites `j`'s fourth index because `j` and `g` point to the *same underlying array*. The `append()` function only creates a new array when there isn't any capacity left. We created `i` with a length of 3 and a capactiy of 8, which means we can append `5` items before a new array is automatically allocated. + +Again, to avoid bugs like this, you should always use the `append` function on the same slice the result is assigned to: + +```go +mySlice := []int{1, 2, 3} +mySlice = append(mySlice, 4) +``` diff --git a/course/8-slices/exercises/11-range/code.go b/course/8-slices/exercises/11-range/code.go new file mode 100644 index 0000000..a89fc21 --- /dev/null +++ b/course/8-slices/exercises/11-range/code.go @@ -0,0 +1,31 @@ +package main + +import "fmt" + +func indexOfFirstBadWord(msg []string, badWords []string) int { + // ? +} + +// don't touch below this line + +func test(msg []string, badWords []string) { + i := indexOfFirstBadWord(msg, badWords) + fmt.Printf("Scanning message: %v for bad words:\n", msg) + for _, badWord := range badWords { + fmt.Println(` -`, badWord) + } + fmt.Printf("Index: %v\n", i) + fmt.Println("====================================") +} + +func main() { + badWords := []string{"crap", "shoot", "dang", "frick"} + message := []string{"hey", "there", "john"} + test(message, badWords) + + message = []string{"ugh", "oh", "my", "frick"} + test(message, badWords) + + message = []string{"what", "the", "shoot", "I", "hate", "that", "crap"} + test(message, badWords) +} diff --git a/course/8-slices/exercises/11-range/complete.go b/course/8-slices/exercises/11-range/complete.go new file mode 100644 index 0000000..ad23ecb --- /dev/null +++ b/course/8-slices/exercises/11-range/complete.go @@ -0,0 +1,38 @@ +package main + +import "fmt" + +func indexOfFirstBadWord(msg []string, badWords []string) int { + for i, word := range msg { + for _, badWord := range badWords { + if word == badWord { + return i + } + } + } + return -1 +} + +// don't touch below this line + +func test(msg []string, badWords []string) { + i := indexOfFirstBadWord(msg, badWords) + fmt.Printf("Scanning message: %v for bad words:\n", msg) + for _, badWord := range badWords { + fmt.Println(` -`, badWord) + } + fmt.Printf("Index: %v\n", i) + fmt.Println("====================================") +} + +func main() { + badWords := []string{"crap", "shoot", "dang", "frick"} + message := []string{"hey", "there", "john"} + test(message, badWords) + + message = []string{"ugh", "oh", "my", "frick"} + test(message, badWords) + + message = []string{"what", "the", "shoot", "I", "hate", "that", "crap"} + test(message, badWords) +} diff --git a/course/8-slices/exercises/11-range/expected.txt b/course/8-slices/exercises/11-range/expected.txt new file mode 100644 index 0000000..c623ada --- /dev/null +++ b/course/8-slices/exercises/11-range/expected.txt @@ -0,0 +1,21 @@ +Scanning message: [hey there john] for bad words: + - crap + - shoot + - dang + - frick +Index: -1 +==================================== +Scanning message: [ugh oh my frick] for bad words: + - crap + - shoot + - dang + - frick +Index: 3 +==================================== +Scanning message: [what the shoot I hate that crap] for bad words: + - crap + - shoot + - dang + - frick +Index: 2 +==================================== diff --git a/course/8-slices/exercises/11-range/readme.md b/course/8-slices/exercises/11-range/readme.md new file mode 100644 index 0000000..74b8108 --- /dev/null +++ b/course/8-slices/exercises/11-range/readme.md @@ -0,0 +1,28 @@ +# Range + +Go provides syntactic sugar to iterate easily over elements of a slice: + +```go +for INDEX, ELEMENT := range SLICE { +} +``` + +For example: + +```go +fruits := []string{"apple", "banana", "grape"} +for i, fruit := range fruits { + fmt.Println(i, fruit) +} +// 0 apple +// 1 banana +// 2 grape +``` + +## Assignment + +We need to be able to quickly detect bad words in the messages our system sends. + +Complete the `indexOfFirstBadWord` function. If it finds any bad words in the message it should return the **index** of the first bad word in the `msg` slice. This will help us filter out naughty words from our messaging system. If no bad words are found, return `-1` instead. + +Use the `range` keyword. diff --git a/course/8-slices/exercises/2-slices/code.go b/course/8-slices/exercises/2-slices/code.go new file mode 100644 index 0000000..34244b1 --- /dev/null +++ b/course/8-slices/exercises/2-slices/code.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" +) + +const ( + planFree = "free" + planPro = "pro" +) + +func getMessageWithRetriesForPlan(plan string) ([]string, error) { + allMessages := getMessageWithRetries() + // ? +} + +// don't touch below this line + +func getMessageWithRetries() [3]string { + return [3]string{ + "click here to sign up", + "pretty please click here", + "we beg you to sign up", + } +} + +func test(name string, doneAt int, plan string) { + defer fmt.Println("=====================================") + fmt.Printf("sending to %v...", name) + fmt.Println() + + messages, err := getMessageWithRetriesForPlan(plan) + if err != nil { + fmt.Println("Error:", err) + return + } + for i := 0; i < len(messages); i++ { + msg := messages[i] + fmt.Printf(`sending: "%v"`, msg) + fmt.Println() + if i == doneAt { + fmt.Println("they responded!") + break + } + if i == len(messages)-1 { + fmt.Println("no response") + } + } +} + +func main() { + test("Ozgur", 3, planFree) + test("Jeff", 3, planPro) + test("Sally", 2, planPro) + test("Sally", 3, "no plan") +} diff --git a/course/8-slices/exercises/2-slices/complete.go b/course/8-slices/exercises/2-slices/complete.go new file mode 100644 index 0000000..a5ed750 --- /dev/null +++ b/course/8-slices/exercises/2-slices/complete.go @@ -0,0 +1,63 @@ +package main + +import ( + "errors" + "fmt" +) + +const ( + planFree = "free" + planPro = "pro" +) + +func getMessageWithRetriesForPlan(plan string) ([]string, error) { + allMessages := getMessageWithRetries() + if plan == planPro { + return allMessages[:], nil + } + if plan == planFree { + return allMessages[0:2], nil + } + return nil, errors.New("unsupported plan") +} + +// don't touch below this line + +func getMessageWithRetries() [3]string { + return [3]string{ + "click here to sign up", + "pretty please click here", + "we beg you to sign up", + } +} + +func test(name string, doneAt int, plan string) { + defer fmt.Println("=====================================") + fmt.Printf("sending to %v...", name) + fmt.Println() + + messages, err := getMessageWithRetriesForPlan(plan) + if err != nil { + fmt.Println("Error:", err) + return + } + for i := 0; i < len(messages); i++ { + msg := messages[i] + fmt.Printf(`sending: "%v"`, msg) + fmt.Println() + if i == doneAt { + fmt.Println("they responded!") + break + } + if i == len(messages)-1 { + fmt.Println("no response") + } + } +} + +func main() { + test("Ozgur", 3, planFree) + test("Jeff", 3, planPro) + test("Sally", 2, planPro) + test("Sally", 3, "no plan") +} diff --git a/course/8-slices/exercises/2-slices/expected.txt b/course/8-slices/exercises/2-slices/expected.txt new file mode 100644 index 0000000..5c50af7 --- /dev/null +++ b/course/8-slices/exercises/2-slices/expected.txt @@ -0,0 +1,20 @@ +sending to Ozgur... +sending: "click here to sign up" +sending: "pretty please click here" +no response +===================================== +sending to Jeff... +sending: "click here to sign up" +sending: "pretty please click here" +sending: "we beg you to sign up" +no response +===================================== +sending to Sally... +sending: "click here to sign up" +sending: "pretty please click here" +sending: "we beg you to sign up" +they responded! +===================================== +sending to Sally... +Error: unsupported plan +===================================== diff --git a/course/8-slices/exercises/2-slices/readme.md b/course/8-slices/exercises/2-slices/readme.md new file mode 100644 index 0000000..a9cb186 --- /dev/null +++ b/course/8-slices/exercises/2-slices/readme.md @@ -0,0 +1,38 @@ +# Slices in Go + +*99 times out of 100* you will use a slice instead of an array when working with ordered lists. + +Arrays are fixed in size. Once you make an array like `[10]int` you can't add an 11th element. + +A slice is a *dynamically-sized*, *flexible* view of the elements of an array. + +Slices **always** have an underlying array, though it isn't always specified explicitly. To explicitly create a slice on top of an array we can do: + +```go +primes := [6]int{2, 3, 5, 7, 11, 13} +mySlice := primes[1:4] +// mySlice = {3, 5, 7} +``` + +The syntax is: + +``` +arrayname[lowIndex:highIndex] +arrayname[lowIndex:] +arrayname[:highIndex] +arrayname[:] +``` + +Where `lowIndex` is inclusive and `highIndex` is exclusive + +Either `lowIndex` or `highIndex` or both can be omitted to use the entire array on that side. + +## Assignment + +Retries are a premium feature now! Textio's free users only get 1 retry message, while pro members get an unlimited amount. + +Complete the `getMessageWithRetriesForPlan` function. It takes a `plan` variable as input, and you've been provided with constants for the plan types at the top of the program. + +* If the plan is a pro plan, return all the strings from `getMessageWithRetries()`. +* If the plan is a free plan, return the first 2 strings from `getMessageWithRetries()`. +* If the plan isn't either of those, return an error that says `unsupported plan`. diff --git a/course/8-slices/exercises/3-slices_quiz/multiple_choice.json b/course/8-slices/exercises/3-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..b175b2a --- /dev/null +++ b/course/8-slices/exercises/3-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which references the other?", + "answers": [ + "Slices reference arrays", + "Arrays reference slices" + ] +} diff --git a/course/8-slices/exercises/3-slices_quiz/readme.md b/course/8-slices/exercises/3-slices_quiz/readme.md new file mode 100644 index 0000000..5304762 --- /dev/null +++ b/course/8-slices/exercises/3-slices_quiz/readme.md @@ -0,0 +1,11 @@ +# Slices Review + +Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays. + +Slices hold references to an underlying array, and if you assign one slice to another, both refer to the **same** array. If a function takes a slice argument, changes it makes to the elements of the slice *will be visible to the caller*, analogous to passing a pointer to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the [Read()](https://pkg.go.dev/os#File.Read) method of the `File` type in package `os`: + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func (f *File) Read(buf []byte) (n int, err error) +``` diff --git a/course/8-slices/exercises/3a-slices_quiz/multiple_choice.json b/course/8-slices/exercises/3a-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..8cac3dc --- /dev/null +++ b/course/8-slices/exercises/3a-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Multiple slices can point to the same array", + "answers": [ + "True", + "False" + ] +} diff --git a/course/8-slices/exercises/3a-slices_quiz/readme.md b/course/8-slices/exercises/3a-slices_quiz/readme.md new file mode 100644 index 0000000..5304762 --- /dev/null +++ b/course/8-slices/exercises/3a-slices_quiz/readme.md @@ -0,0 +1,11 @@ +# Slices Review + +Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays. + +Slices hold references to an underlying array, and if you assign one slice to another, both refer to the **same** array. If a function takes a slice argument, changes it makes to the elements of the slice *will be visible to the caller*, analogous to passing a pointer to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the [Read()](https://pkg.go.dev/os#File.Read) method of the `File` type in package `os`: + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func (f *File) Read(buf []byte) (n int, err error) +``` diff --git a/course/8-slices/exercises/3b-slices_quiz/multiple_choice.json b/course/8-slices/exercises/3b-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..3108f0c --- /dev/null +++ b/course/8-slices/exercises/3b-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "A function that only has access to a slice can modify the underlying array", + "answers": [ + "True", + "False" + ] +} diff --git a/course/8-slices/exercises/3b-slices_quiz/readme.md b/course/8-slices/exercises/3b-slices_quiz/readme.md new file mode 100644 index 0000000..5304762 --- /dev/null +++ b/course/8-slices/exercises/3b-slices_quiz/readme.md @@ -0,0 +1,11 @@ +# Slices Review + +Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays. + +Slices hold references to an underlying array, and if you assign one slice to another, both refer to the **same** array. If a function takes a slice argument, changes it makes to the elements of the slice *will be visible to the caller*, analogous to passing a pointer to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the [Read()](https://pkg.go.dev/os#File.Read) method of the `File` type in package `os`: + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func (f *File) Read(buf []byte) (n int, err error) +``` diff --git a/course/8-slices/exercises/4-slices_no_array/code.go b/course/8-slices/exercises/4-slices_no_array/code.go new file mode 100644 index 0000000..494737b --- /dev/null +++ b/course/8-slices/exercises/4-slices_no_array/code.go @@ -0,0 +1,48 @@ +package main + +import "fmt" + +func getMessageCosts(messages []string) []float64 { + // ? +} + +// don't edit below this line + +func test(messages []string) { + costs := getMessageCosts(messages) + fmt.Println("Messages:") + for i := 0; i < len(messages); i++ { + fmt.Printf(" - %v\n", messages[i]) + } + fmt.Println("Costs:") + for i := 0; i < len(costs); i++ { + fmt.Printf(" - %.2f\n", costs[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test([]string{ + "Welcome to the movies!", + "Enjoy your popcorn!", + "Please don't talk during the movie!", + }) + test([]string{ + "I don't want to be here anymore", + "Can we go home?", + "I'm hungry", + "I'm bored", + }) + test([]string{ + "Hello", + "Hi", + "Hey", + "Hi there", + "Hey there", + "Hi there", + "Hello there", + "Hey there", + "Hello there", + "General Kenobi", + }) +} diff --git a/course/8-slices/exercises/4-slices_no_array/complete.go b/course/8-slices/exercises/4-slices_no_array/complete.go new file mode 100644 index 0000000..69c3b6e --- /dev/null +++ b/course/8-slices/exercises/4-slices_no_array/complete.go @@ -0,0 +1,53 @@ +package main + +import "fmt" + +func getMessageCosts(messages []string) []float64 { + messageCosts := make([]float64, len(messages)) + for i := 0; i < len(messages); i++ { + cost := float64(len(messages[i])) * 0.01 + messageCosts[i] = cost + } + return messageCosts +} + +// don't edit below this line + +func test(messages []string) { + costs := getMessageCosts(messages) + fmt.Println("Messages:") + for i := 0; i < len(messages); i++ { + fmt.Printf(" - %v\n", messages[i]) + } + fmt.Println("Costs:") + for i := 0; i < len(costs); i++ { + fmt.Printf(" - %.2f\n", costs[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test([]string{ + "Welcome to the movies!", + "Enjoy your popcorn!", + "Please don't talk during the movie!", + }) + test([]string{ + "I don't want to be here anymore", + "Can we go home?", + "I'm hungry", + "I'm bored", + }) + test([]string{ + "Hello", + "Hi", + "Hey", + "Hi there", + "Hey there", + "Hi there", + "Hello there", + "Hey there", + "Hello there", + "General Kenobi", + }) +} diff --git a/course/8-slices/exercises/4-slices_no_array/expected.txt b/course/8-slices/exercises/4-slices_no_array/expected.txt new file mode 100644 index 0000000..9a18bbd --- /dev/null +++ b/course/8-slices/exercises/4-slices_no_array/expected.txt @@ -0,0 +1,43 @@ +Messages: + - Welcome to the movies! + - Enjoy your popcorn! + - Please don't talk during the movie! +Costs: + - 0.22 + - 0.19 + - 0.35 +===== END REPORT ===== +Messages: + - I don't want to be here anymore + - Can we go home? + - I'm hungry + - I'm bored +Costs: + - 0.31 + - 0.15 + - 0.10 + - 0.09 +===== END REPORT ===== +Messages: + - Hello + - Hi + - Hey + - Hi there + - Hey there + - Hi there + - Hello there + - Hey there + - Hello there + - General Kenobi +Costs: + - 0.05 + - 0.02 + - 0.03 + - 0.08 + - 0.09 + - 0.08 + - 0.11 + - 0.09 + - 0.11 + - 0.14 +===== END REPORT ===== diff --git a/course/8-slices/exercises/4-slices_no_array/readme.md b/course/8-slices/exercises/4-slices_no_array/readme.md new file mode 100644 index 0000000..cb048e3 --- /dev/null +++ b/course/8-slices/exercises/4-slices_no_array/readme.md @@ -0,0 +1,52 @@ +# Make + +Most of the time we don't need to think about the underlying array of a slice. We can create a new slice using the `make` function: + +```go +// func make([]T, len, cap) []T +mySlice := make([]int, 5, 10) + +// the capacity argument is usually omitted and defaults to the length +mySlice := make([]int, 5) +``` + +Slices created with `make` will be filled with the zero value of the type. + +If we want to create a slice with a specific set of values, we can use a slice literal: + +```go +mySlice := []string{"I", "love", "go"} +``` + +Note that the array brackets *do not* have a `3` in them. If they did, you'd have an *array* instead of a slice. + +## Length + +The length of a slice is simply the number of elements it contains. It is accessed using the built-in `len()` function: + +```go +mySlice := []string{"I", "love", "go"} +fmt.Println(len(mySlice)) // 3 +``` + +## Capacity + +The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice. It is accessed using the built-in `cap()` function: + +```go +mySlice := []string{"I", "love", "go"} +fmt.Println(cap(mySlice)) // 3 +``` + +Generally speaking, unless you're hyper-optimizing the memory usage of your program, you don't need to worry about the capacity of a slice because it will automatically grow as needed. + +## Assignment + +We send a lot of text messages at Textio, and our API is getting slow and unresponsive. + +If we know the rough size of a slice before we fill it up, we can make our program faster by creating the slice with that size *ahead of time* so that the Go runtime doesn't need to continuously allocate new underlying arrays of larger and larger sizes. By setting the length, the slice can still be resized later, but it means we can avoid all the expensive resizing we **know** that we'll need. + +Complete the `getMessageCosts()` function. It takes a slice of messages and returns a slice of message costs. + +1. Preallocate a slice for the message costs of the same length as the `messages` slice. +2. Fill the costs slice with costs for each message. The cost in the cost slice should correspond to the message in the messages slice at the same index. The cost of a message is the length of the message multiplied by `0.01`. diff --git a/course/8-slices/exercises/5-slices_quiz/multiple_choice.json b/course/8-slices/exercises/5-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..1dbdfdd --- /dev/null +++ b/course/8-slices/exercises/5-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "What does the cap() function return?", + "answers": [ + "The maximum length of the slice before reallocation of the array is necessary", + "The last element of the slice" + ] +} diff --git a/course/8-slices/exercises/5-slices_quiz/readme.md b/course/8-slices/exercises/5-slices_quiz/readme.md new file mode 100644 index 0000000..0c297b1 --- /dev/null +++ b/course/8-slices/exercises/5-slices_quiz/readme.md @@ -0,0 +1,21 @@ +# Len and Cap Review + +The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The *capacity* of a slice, accessible by the built-in function `cap`, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that `len` and `cap` are legal when applied to the `nil` slice, and return `0`. + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func Append(slice, data []byte) []byte { + l := len(slice) + if l + len(data) > cap(slice) { // reallocate + // Allocate double what's needed, for future growth. + newSlice := make([]byte, (l+len(data))*2) + // The copy function is predeclared and works for any slice type. + copy(newSlice, slice) + slice = newSlice + } + slice = slice[0:l+len(data)] + copy(slice[l:], data) + return slice +} +``` diff --git a/course/8-slices/exercises/5a-slices_quiz/multiple_choice.json b/course/8-slices/exercises/5a-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..ff50203 --- /dev/null +++ b/course/8-slices/exercises/5a-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "What does the len() function return?", + "answers": [ + "The current length of the slice", + "The maximum length of the slice before reallocation of the array is necessary" + ] +} diff --git a/course/8-slices/exercises/5a-slices_quiz/readme.md b/course/8-slices/exercises/5a-slices_quiz/readme.md new file mode 100644 index 0000000..0c297b1 --- /dev/null +++ b/course/8-slices/exercises/5a-slices_quiz/readme.md @@ -0,0 +1,21 @@ +# Len and Cap Review + +The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The *capacity* of a slice, accessible by the built-in function `cap`, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that `len` and `cap` are legal when applied to the `nil` slice, and return `0`. + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func Append(slice, data []byte) []byte { + l := len(slice) + if l + len(data) > cap(slice) { // reallocate + // Allocate double what's needed, for future growth. + newSlice := make([]byte, (l+len(data))*2) + // The copy function is predeclared and works for any slice type. + copy(newSlice, slice) + slice = newSlice + } + slice = slice[0:l+len(data)] + copy(slice[l:], data) + return slice +} +``` diff --git a/course/8-slices/exercises/5b-slices_quiz/multiple_choice.json b/course/8-slices/exercises/5b-slices_quiz/multiple_choice.json new file mode 100644 index 0000000..abc1082 --- /dev/null +++ b/course/8-slices/exercises/5b-slices_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "What do len() and cap() do when a slice is nil?", + "answers": [ + "Return 0", + "Panic" + ] +} diff --git a/course/8-slices/exercises/5b-slices_quiz/readme.md b/course/8-slices/exercises/5b-slices_quiz/readme.md new file mode 100644 index 0000000..0c297b1 --- /dev/null +++ b/course/8-slices/exercises/5b-slices_quiz/readme.md @@ -0,0 +1,21 @@ +# Len and Cap Review + +The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The *capacity* of a slice, accessible by the built-in function `cap`, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that `len` and `cap` are legal when applied to the `nil` slice, and return `0`. + +Referenced from [Effective Go](https://golang.org/doc/effective_go.html#slices) + +```go +func Append(slice, data []byte) []byte { + l := len(slice) + if l + len(data) > cap(slice) { // reallocate + // Allocate double what's needed, for future growth. + newSlice := make([]byte, (l+len(data))*2) + // The copy function is predeclared and works for any slice type. + copy(newSlice, slice) + slice = newSlice + } + slice = slice[0:l+len(data)] + copy(slice[l:], data) + return slice +} +``` diff --git a/course/8-slices/exercises/7-variadic_functions/code.go b/course/8-slices/exercises/7-variadic_functions/code.go new file mode 100644 index 0000000..8a1e367 --- /dev/null +++ b/course/8-slices/exercises/7-variadic_functions/code.go @@ -0,0 +1,23 @@ +package main + +import "fmt" + +func sum(nums ...float64) float64 { + // ? +} + +// don't edit below this line + +func test(nums ...float64) { + total := sum(nums...) + fmt.Printf("Summing %v costs...\n", len(nums)) + fmt.Printf("Bill for the month: %.2f\n", total) + fmt.Println("===== END REPORT =====") +} + +func main() { + test(1.0, 2.0, 3.0) + test(1.0, 2.0, 3.0, 4.0, 5.0) + test(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0) + test(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0) +} diff --git a/course/8-slices/exercises/7-variadic_functions/complete.go b/course/8-slices/exercises/7-variadic_functions/complete.go new file mode 100644 index 0000000..ea324d4 --- /dev/null +++ b/course/8-slices/exercises/7-variadic_functions/complete.go @@ -0,0 +1,28 @@ +package main + +import "fmt" + +func sum(nums ...float64) float64 { + sum := 0.0 + for i := 0; i < len(nums); i++ { + num := nums[i] + sum += num + } + return sum +} + +// don't edit below this line + +func test(nums ...float64) { + total := sum(nums...) + fmt.Printf("Summing %v costs...\n", len(nums)) + fmt.Printf("Bill for the month: %.2f\n", total) + fmt.Println("===== END REPORT =====") +} + +func main() { + test(1.0, 2.0, 3.0) + test(1.0, 2.0, 3.0, 4.0, 5.0) + test(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0) + test(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0) +} diff --git a/course/8-slices/exercises/7-variadic_functions/expected.txt b/course/8-slices/exercises/7-variadic_functions/expected.txt new file mode 100644 index 0000000..679ac65 --- /dev/null +++ b/course/8-slices/exercises/7-variadic_functions/expected.txt @@ -0,0 +1,12 @@ +Summing 3 costs... +Bill for the month: 6.00 +===== END REPORT ===== +Summing 5 costs... +Bill for the month: 15.00 +===== END REPORT ===== +Summing 10 costs... +Bill for the month: 55.00 +===== END REPORT ===== +Summing 15 costs... +Bill for the month: 120.00 +===== END REPORT ===== diff --git a/course/8-slices/exercises/7-variadic_functions/readme.md b/course/8-slices/exercises/7-variadic_functions/readme.md new file mode 100644 index 0000000..5fc7baf --- /dev/null +++ b/course/8-slices/exercises/7-variadic_functions/readme.md @@ -0,0 +1,53 @@ +# Variadic + +Many functions, especially those in the standard library, can take an arbitrary number of *final* arguments. This is accomplished by using the "..." syntax in the function signature. + +A variadic function receives the variadic arguments as a slice. + +```go +func sum(nums ...int) int { + // nums is just a slice + for i := 0; i < len(nums); i++{ + num := nums[i] + } +} + +func main() { + total := sum(1, 2, 3) + fmt.Println(total) + // prints "6" +} +``` + +The familiar [fmt.Println()](https://pkg.go.dev/fmt#Println) and [fmt.Sprintf()](https://pkg.go.dev/fmt#Sprintf) are variadic! `fmt.Println()` prints each element with space [delimiters](https://www.dictionary.com/browse/delimited) and a newline at the end. + +```go +func Println(a ...interface{}) (n int, err error) +``` + +## Spread operator + +The spread operator allows us to pass a slice *into* a variadic function. The spread operator consists of three dots following the slice in the function call. + +```go +func printStrings(strings ...string) { + for i := 0; i < len(strings); i++ { + fmt.Println(strings[i]) + } +} + +func main() { + names := []string{"bob", "sue", "alice"} + printStrings(names...) +} +``` + +## Assignment + +We need to sum up the costs of all individual messages so we can send an end-of-month bill to our customers. + +Complete the `sum` function so that returns the sum of all of its inputs. + +## Note + +Make note of how the variadic inputs and the spread operator are used in the test suite. diff --git a/course/8-slices/exercises/8-append/code.go b/course/8-slices/exercises/8-append/code.go new file mode 100644 index 0000000..b563329 --- /dev/null +++ b/course/8-slices/exercises/8-append/code.go @@ -0,0 +1,49 @@ +package main + +import "fmt" + +type cost struct { + day int + value float64 +} + +func getCostsByDay(costs []cost) []float64 { + // ? +} + +// dont edit below this line + +func test(costs []cost) { + fmt.Printf("Creating daily buckets for %v costs...\n", len(costs)) + costsByDay := getCostsByDay(costs) + fmt.Println("Costs by day:") + for i := 0; i < len(costsByDay); i++ { + fmt.Printf(" - Day %v: %.2f\n", i, costsByDay[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test([]cost{ + {0, 1.0}, + {1, 2.0}, + {1, 3.1}, + {2, 2.5}, + {3, 3.6}, + {3, 2.7}, + {4, 3.34}, + }) + test([]cost{ + {0, 1.0}, + {10, 2.0}, + {3, 3.1}, + {2, 2.5}, + {1, 3.6}, + {2, 2.7}, + {4, 56.34}, + {13, 2.34}, + {28, 1.34}, + {25, 2.34}, + {30, 4.34}, + }) +} diff --git a/course/8-slices/exercises/8-append/complete.go b/course/8-slices/exercises/8-append/complete.go new file mode 100644 index 0000000..b62c1d4 --- /dev/null +++ b/course/8-slices/exercises/8-append/complete.go @@ -0,0 +1,57 @@ +package main + +import "fmt" + +type cost struct { + day int + value float64 +} + +func getCostsByDay(costs []cost) []float64 { + costsByDay := []float64{} + for i := 0; i < len(costs); i++ { + cost := costs[i] + for cost.day >= len(costsByDay) { + costsByDay = append(costsByDay, 0.0) + } + costsByDay[cost.day] += cost.value + } + return costsByDay +} + +// dont edit below this line + +func test(costs []cost) { + fmt.Printf("Creating daily buckets for %v costs...\n", len(costs)) + costsByDay := getCostsByDay(costs) + fmt.Println("Costs by day:") + for i := 0; i < len(costsByDay); i++ { + fmt.Printf(" - Day %v: %.2f\n", i, costsByDay[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test([]cost{ + {0, 1.0}, + {1, 2.0}, + {1, 3.1}, + {2, 2.5}, + {3, 3.6}, + {3, 2.7}, + {4, 3.34}, + }) + test([]cost{ + {0, 1.0}, + {10, 2.0}, + {3, 3.1}, + {2, 2.5}, + {1, 3.6}, + {2, 2.7}, + {4, 56.34}, + {13, 2.34}, + {28, 1.34}, + {25, 2.34}, + {30, 4.34}, + }) +} diff --git a/course/8-slices/exercises/8-append/expected.txt b/course/8-slices/exercises/8-append/expected.txt new file mode 100644 index 0000000..fea32c5 --- /dev/null +++ b/course/8-slices/exercises/8-append/expected.txt @@ -0,0 +1,42 @@ +Creating daily buckets for 7 costs... +Costs by day: + - Day 0: 1.00 + - Day 1: 5.10 + - Day 2: 2.50 + - Day 3: 6.30 + - Day 4: 3.34 +===== END REPORT ===== +Creating daily buckets for 11 costs... +Costs by day: + - Day 0: 1.00 + - Day 1: 3.60 + - Day 2: 5.20 + - Day 3: 3.10 + - Day 4: 56.34 + - Day 5: 0.00 + - Day 6: 0.00 + - Day 7: 0.00 + - Day 8: 0.00 + - Day 9: 0.00 + - Day 10: 2.00 + - Day 11: 0.00 + - Day 12: 0.00 + - Day 13: 2.34 + - Day 14: 0.00 + - Day 15: 0.00 + - Day 16: 0.00 + - Day 17: 0.00 + - Day 18: 0.00 + - Day 19: 0.00 + - Day 20: 0.00 + - Day 21: 0.00 + - Day 22: 0.00 + - Day 23: 0.00 + - Day 24: 0.00 + - Day 25: 2.34 + - Day 26: 0.00 + - Day 27: 0.00 + - Day 28: 1.34 + - Day 29: 0.00 + - Day 30: 4.34 +===== END REPORT ===== diff --git a/course/8-slices/exercises/8-append/readme.md b/course/8-slices/exercises/8-append/readme.md new file mode 100644 index 0000000..7ff700e --- /dev/null +++ b/course/8-slices/exercises/8-append/readme.md @@ -0,0 +1,51 @@ +# Append + +The built-in append function is used to dynamically add elements to a slice: + +```go +func append(slice []Type, elems ...Type) []Type +``` + +If the underlying array is not large enough, `append()` will create a new underlying array and point the slice to it. + +Notice that `append()` is variadic, the following are all valid: + +```go +slice = append(slice, oneThing) +slice = append(slice, firstThing, secondThing) +slice = append(slice, anotherSlice...) +``` + +## Assignment + +We've been asked to "bucket" costs for an entire month into the cost occurring on each day of the month. + +Complete the `getCostsByDay` function. It should return a slice of `float64`s, where each element is the total cost for that day. The length of the slice should be equal to the number of days represented in the `costs` slice, including any days that have no costs, up to the last day represented in the slice. + +Days are numbered and start at `0`. + +### Example + +Input in `day, cost` format: + +```go +[]cost{ + {0, 4.0}, + {1, 2.1}, + {1, 3.1}, + {5, 2.5}, +} +``` + +I would expect this result: + +```go +[]float64{ + 4.0, + 5.2, + 0.0, + 0.0, + 0.0, + 2.5, +} +``` diff --git a/course/8-slices/exercises/9-2d_slices/code.go b/course/8-slices/exercises/9-2d_slices/code.go new file mode 100644 index 0000000..f69d363 --- /dev/null +++ b/course/8-slices/exercises/9-2d_slices/code.go @@ -0,0 +1,25 @@ +package main + +import "fmt" + +func createMatrix(rows, cols int) [][]int { + // ? +} + +// dont edit below this line + +func test(rows, cols int) { + fmt.Printf("Creating %v x %v matrix...\n", rows, cols) + matrix := createMatrix(rows, cols) + for i := 0; i < len(matrix); i++ { + fmt.Println(matrix[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test(3, 3) + test(5, 5) + test(10, 10) + test(15, 15) +} diff --git a/course/8-slices/exercises/9-2d_slices/complete.go b/course/8-slices/exercises/9-2d_slices/complete.go new file mode 100644 index 0000000..483b025 --- /dev/null +++ b/course/8-slices/exercises/9-2d_slices/complete.go @@ -0,0 +1,33 @@ +package main + +import "fmt" + +func createMatrix(rows, cols int) [][]int { + matrix := [][]int{} + for i := 0; i < rows; i++ { + row := []int{} + for j := 0; j < cols; j++ { + row = append(row, i*j) + } + matrix = append(matrix, row) + } + return matrix +} + +// dont edit below this line + +func test(rows, cols int) { + fmt.Printf("Creating %v x %v matrix...\n", rows, cols) + matrix := createMatrix(rows, cols) + for i := 0; i < len(matrix); i++ { + fmt.Println(matrix[i]) + } + fmt.Println("===== END REPORT =====") +} + +func main() { + test(3, 3) + test(5, 5) + test(10, 10) + test(15, 15) +} diff --git a/course/8-slices/exercises/9-2d_slices/expected.txt b/course/8-slices/exercises/9-2d_slices/expected.txt new file mode 100644 index 0000000..a8e7913 --- /dev/null +++ b/course/8-slices/exercises/9-2d_slices/expected.txt @@ -0,0 +1,41 @@ +Creating 3 x 3 matrix... +[0 0 0] +[0 1 2] +[0 2 4] +===== END REPORT ===== +Creating 5 x 5 matrix... +[0 0 0 0 0] +[0 1 2 3 4] +[0 2 4 6 8] +[0 3 6 9 12] +[0 4 8 12 16] +===== END REPORT ===== +Creating 10 x 10 matrix... +[0 0 0 0 0 0 0 0 0 0] +[0 1 2 3 4 5 6 7 8 9] +[0 2 4 6 8 10 12 14 16 18] +[0 3 6 9 12 15 18 21 24 27] +[0 4 8 12 16 20 24 28 32 36] +[0 5 10 15 20 25 30 35 40 45] +[0 6 12 18 24 30 36 42 48 54] +[0 7 14 21 28 35 42 49 56 63] +[0 8 16 24 32 40 48 56 64 72] +[0 9 18 27 36 45 54 63 72 81] +===== END REPORT ===== +Creating 15 x 15 matrix... +[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] +[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14] +[0 2 4 6 8 10 12 14 16 18 20 22 24 26 28] +[0 3 6 9 12 15 18 21 24 27 30 33 36 39 42] +[0 4 8 12 16 20 24 28 32 36 40 44 48 52 56] +[0 5 10 15 20 25 30 35 40 45 50 55 60 65 70] +[0 6 12 18 24 30 36 42 48 54 60 66 72 78 84] +[0 7 14 21 28 35 42 49 56 63 70 77 84 91 98] +[0 8 16 24 32 40 48 56 64 72 80 88 96 104 112] +[0 9 18 27 36 45 54 63 72 81 90 99 108 117 126] +[0 10 20 30 40 50 60 70 80 90 100 110 120 130 140] +[0 11 22 33 44 55 66 77 88 99 110 121 132 143 154] +[0 12 24 36 48 60 72 84 96 108 120 132 144 156 168] +[0 13 26 39 52 65 78 91 104 117 130 143 156 169 182] +[0 14 28 42 56 70 84 98 112 126 140 154 168 182 196] +===== END REPORT ===== diff --git a/course/8-slices/exercises/9-2d_slices/readme.md b/course/8-slices/exercises/9-2d_slices/readme.md new file mode 100644 index 0000000..1c2ae30 --- /dev/null +++ b/course/8-slices/exercises/9-2d_slices/readme.md @@ -0,0 +1,28 @@ +# Slice of slices + +Slices can hold other slices, effectively creating a [matrix](https://en.wikipedia.org/wiki/Matrix_(mathematics)), or a 2D slice. + +```go +rows := [][]int{} +``` + +## Assignment + +We support various graphs and dashboards on Textio that display message analytics to our users. The UI for our graphs and charts is built on top of a grid system. Let's build some grid logic. + +Complete the `createMatrix` function. It takes a number of rows and columns and returns a 2D slice of integers where the value of each cell is `i * j` where `i` and `j` are the indexes of the row and column respectively. + +For example, a 10x10 matrix would look like this: + +``` +[0 0 0 0 0 0 0 0 0 0] +[0 1 2 3 4 5 6 7 8 9] +[0 2 4 6 8 10 12 14 16 18] +[0 3 6 9 12 15 18 21 24 27] +[0 4 8 12 16 20 24 28 32 36] +[0 5 10 15 20 25 30 35 40 45] +[0 6 12 18 24 30 36 42 48 54] +[0 7 14 21 28 35 42 49 56 63] +[0 8 16 24 32 40 48 56 64 72] +[0 9 18 27 36 45 54 63 72 81] +``` diff --git a/course/9-maps/exercises/1-maps/code.go b/course/9-maps/exercises/1-maps/code.go new file mode 100644 index 0000000..e6eac28 --- /dev/null +++ b/course/9-maps/exercises/1-maps/code.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" +) + +func getUserMap(names []string, phoneNumbers []int) (map[string]user, error) { + // ? +} + +// don't touch below this line + +type user struct { + name string + phoneNumber int +} + +func test(names []string, phoneNumbers []int) { + fmt.Println("Creating map...") + defer fmt.Println("====================================") + users, err := getUserMap(names, phoneNumbers) + if err != nil { + fmt.Println(err) + return + } + for _, name := range names { + fmt.Printf("key: %v, value:\n", name) + fmt.Println(" - name:", users[name].name) + fmt.Println(" - number:", users[name].phoneNumber) + } +} + +func main() { + test( + []string{"John", "Bob", "Jill"}, + []int{14355550987, 98765550987, 18265554567}, + ) + test( + []string{"John", "Bob"}, + []int{14355550987, 98765550987, 18265554567}, + ) + test( + []string{"George", "Sally", "Rich", "Sue"}, + []int{20955559812, 38385550982, 48265554567, 16045559873}, + ) +} diff --git a/course/9-maps/exercises/1-maps/complete.go b/course/9-maps/exercises/1-maps/complete.go new file mode 100644 index 0000000..afb8f90 --- /dev/null +++ b/course/9-maps/exercises/1-maps/complete.go @@ -0,0 +1,58 @@ +package main + +import ( + "errors" + "fmt" +) + +func getUserMap(names []string, phoneNumbers []int) (map[string]user, error) { + users := make(map[string]user) + if len(names) != len(phoneNumbers) { + return nil, errors.New("invalid sizes") + } + for i := range names { + name := names[i] + users[name] = user{ + name: name, + phoneNumber: phoneNumbers[i], + } + } + return users, nil +} + +// don't touch below this line + +type user struct { + name string + phoneNumber int +} + +func test(names []string, phoneNumbers []int) { + fmt.Println("Creating map...") + defer fmt.Println("====================================") + users, err := getUserMap(names, phoneNumbers) + if err != nil { + fmt.Println(err) + return + } + for _, name := range names { + fmt.Printf("key: %v, value:\n", name) + fmt.Println(" - name:", users[name].name) + fmt.Println(" - number:", users[name].phoneNumber) + } +} + +func main() { + test( + []string{"John", "Bob", "Jill"}, + []int{14355550987, 98765550987, 18265554567}, + ) + test( + []string{"John", "Bob"}, + []int{14355550987, 98765550987, 18265554567}, + ) + test( + []string{"George", "Sally", "Rich", "Sue"}, + []int{20955559812, 38385550982, 48265554567, 16045559873}, + ) +} diff --git a/course/9-maps/exercises/1-maps/expected.txt b/course/9-maps/exercises/1-maps/expected.txt new file mode 100644 index 0000000..c9795ad --- /dev/null +++ b/course/9-maps/exercises/1-maps/expected.txt @@ -0,0 +1,28 @@ +Creating map... +key: John, value: + - name: John + - number: 14355550987 +key: Bob, value: + - name: Bob + - number: 98765550987 +key: Jill, value: + - name: Jill + - number: 18265554567 +==================================== +Creating map... +invalid sizes +==================================== +Creating map... +key: George, value: + - name: George + - number: 20955559812 +key: Sally, value: + - name: Sally + - number: 38385550982 +key: Rich, value: + - name: Rich + - number: 48265554567 +key: Sue, value: + - name: Sue + - number: 16045559873 +==================================== diff --git a/course/9-maps/exercises/1-maps/readme.md b/course/9-maps/exercises/1-maps/readme.md new file mode 100644 index 0000000..d310eac --- /dev/null +++ b/course/9-maps/exercises/1-maps/readme.md @@ -0,0 +1,41 @@ +# Maps + +Maps are similar to JavaScript objects, Python dictionaries, and Ruby hashes. Maps are a data structure that provides key->value mapping. + +The zero value of a map is `nil`. + +We can create a map by using a literal or by using the `make()` function: + +```go +ages := make(map[string]int) +ages["John"] = 37 +ages["Mary"] = 24 +ages["Mary"] = 21 // overwrites 24 +``` + +```go +ages = map[string]int{ + "John": 37, + "Mary": 21, +} +``` + +The `len()` function works on a map, it returns the total number of key/value pairs. + +```go +ages = map[string]int{ + "John": 37, + "Mary": 21, +} +fmt.Println(len(ages)) // 2 +``` + +## Assignment + +We can speed up our contact-info lookups by using a map! Looking up a value in a map by its key is much faster than searching through a slice. + +Complete the `getUserMap` function. It takes a slice of names and a slice of phone numbers, and returns a map of `name` -> `user` structs and potentially an `error`. A `user` struct just contains a user's name and phone number. + +If the length of `names` and `phoneNumbers` is not equal, return an error with the string "invalid sizes". + +The first name in the `names` slice matches the first phone number, and so on. diff --git a/course/9-maps/exercises/2-mutating_maps/code.go b/course/9-maps/exercises/2-mutating_maps/code.go new file mode 100644 index 0000000..fb3ec20 --- /dev/null +++ b/course/9-maps/exercises/2-mutating_maps/code.go @@ -0,0 +1,73 @@ +package main + +import ( + "fmt" + "sort" +) + +func deleteIfNecessary(users map[string]user, name string) (deleted bool, err error) { + // ? +} + +// don't touch below this line + +type user struct { + name string + number int + scheduledForDeletion bool +} + +func test(users map[string]user, name string) { + fmt.Printf("Attempting to delete %s...\n", name) + defer fmt.Println("====================================") + deleted, err := deleteIfNecessary(users, name) + if err != nil { + fmt.Println(err) + return + } + if deleted { + fmt.Println("Deleted:", name) + return + } + fmt.Println("Did not delete:", name) +} + +func main() { + users := map[string]user{ + "john": { + name: "john", + number: 18965554631, + scheduledForDeletion: true, + }, + "elon": { + name: "elon", + number: 19875556452, + scheduledForDeletion: true, + }, + "breanna": { + name: "breanna", + number: 98575554231, + scheduledForDeletion: false, + }, + "kade": { + name: "kade", + number: 10765557221, + scheduledForDeletion: false, + }, + } + test(users, "john") + test(users, "musk") + test(users, "santa") + test(users, "kade") + + keys := []string{} + for name := range users { + keys = append(keys, name) + } + sort.Strings(keys) + + fmt.Println("Final map keys:") + for _, name := range keys { + fmt.Println(" - ", name) + } +} diff --git a/course/9-maps/exercises/2-mutating_maps/complete.go b/course/9-maps/exercises/2-mutating_maps/complete.go new file mode 100644 index 0000000..a22b8c9 --- /dev/null +++ b/course/9-maps/exercises/2-mutating_maps/complete.go @@ -0,0 +1,82 @@ +package main + +import ( + "errors" + "fmt" + "sort" +) + +func deleteIfNecessary(users map[string]user, name string) (deleted bool, err error) { + user, ok := users[name] + if !ok { + return false, errors.New("not found") + } + if !user.scheduledForDeletion { + return false, nil + } + delete(users, name) + return true, nil +} + +// don't touch below this line + +type user struct { + name string + number int + scheduledForDeletion bool +} + +func test(users map[string]user, name string) { + fmt.Printf("Attempting to delete %s...\n", name) + defer fmt.Println("====================================") + deleted, err := deleteIfNecessary(users, name) + if err != nil { + fmt.Println(err) + return + } + if deleted { + fmt.Println("Deleted:", name) + return + } + fmt.Println("Did not delete:", name) +} + +func main() { + users := map[string]user{ + "john": { + name: "john", + number: 18965554631, + scheduledForDeletion: true, + }, + "elon": { + name: "elon", + number: 19875556452, + scheduledForDeletion: true, + }, + "breanna": { + name: "breanna", + number: 98575554231, + scheduledForDeletion: false, + }, + "kade": { + name: "kade", + number: 10765557221, + scheduledForDeletion: false, + }, + } + test(users, "john") + test(users, "musk") + test(users, "santa") + test(users, "kade") + + keys := []string{} + for name := range users { + keys = append(keys, name) + } + sort.Strings(keys) + + fmt.Println("Final map keys:") + for _, name := range keys { + fmt.Println(" - ", name) + } +} diff --git a/course/9-maps/exercises/2-mutating_maps/expected.txt b/course/9-maps/exercises/2-mutating_maps/expected.txt new file mode 100644 index 0000000..8a00279 --- /dev/null +++ b/course/9-maps/exercises/2-mutating_maps/expected.txt @@ -0,0 +1,16 @@ +Attempting to delete john... +Deleted: john +==================================== +Attempting to delete musk... +not found +==================================== +Attempting to delete santa... +not found +==================================== +Attempting to delete kade... +Did not delete: kade +==================================== +Final map keys: + - breanna + - elon + - kade diff --git a/course/9-maps/exercises/2-mutating_maps/readme.md b/course/9-maps/exercises/2-mutating_maps/readme.md new file mode 100644 index 0000000..7f96953 --- /dev/null +++ b/course/9-maps/exercises/2-mutating_maps/readme.md @@ -0,0 +1,43 @@ +# Mutations + +## Insert and element + +```go +m[key] = elem +``` + +## Get an element + +```go +elem = m[key] +``` + +## Delete an element + +```go +delete(m, key) +``` + +## Check if a key exists + +```go +elem, ok := m[key] +``` + +If `key` is in `m`, then `ok` is `true`. If not, `ok` is `false`. + +If `key` is not in the map, then `elem` is the zero value for the map's element type. + +## Assignment + +It's important to keep up with privacy regulations and to respect our user's data. We need a function that will delete user records. + +Complete the `deleteIfNecessary` function. + +* If the user doesn't exist in the map, return the error `not found`. +* If they exist but aren't scheduled for deletion, return `deleted` as `false` with no errors. +* If they exist and are scheduled for deletion, return `deleted` as `true` with no errors and delete their record from the map. + +## Note on passing maps + +Like slices, maps are also passed by reference into functions. This means that when a map is passed into a function we write, we *can* make changes to the original, we don't have a copy. diff --git a/course/9-maps/exercises/3-map_keys/multiple_choice.json b/course/9-maps/exercises/3-map_keys/multiple_choice.json new file mode 100644 index 0000000..1a89e76 --- /dev/null +++ b/course/9-maps/exercises/3-map_keys/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What makes a type qualify to be used as a map key?", + "answers": [ + "The type is comparable", + "The type is basic", + "The type is Go native; not custom" + ] +} diff --git a/course/9-maps/exercises/3-map_keys/readme.md b/course/9-maps/exercises/3-map_keys/readme.md new file mode 100644 index 0000000..41317d4 --- /dev/null +++ b/course/9-maps/exercises/3-map_keys/readme.md @@ -0,0 +1,54 @@ +# Key Types + +Any type can be used as the *value* in a map, but *keys* are more restrictive. + +Read the following section of the official [Go blog](https://go.dev/blog/maps): + +As mentioned earlier, **map keys may be of any type that is comparable**. The language spec defines this precisely, but in short, comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types. Notably absent from the list are slices, maps, and functions; these types cannot be compared using ==, and may not be used as map keys. + +It's obvious that strings, ints, and other basic types should be available as map keys, but perhaps unexpected are struct keys. Struct can be used to key data by multiple dimensions. For example, this map of maps could be used to tally web page hits by country: + +```go +hits := make(map[string]map[string]int) +``` + +This is map of string to (map of string to int). Each key of the outer map is the path to a web page with its own inner map. Each inner map key is a two-letter country code. This expression retrieves the number of times an Australian has loaded the documentation page: + +```go +n := hits["/doc/"]["au"] +``` + +Unfortunately, this approach becomes unwieldy when adding data, as for any given outer key you must check if the inner map exists, and create it if needed: + +```go +func add(m map[string]map[string]int, path, country string) { + mm, ok := m[path] + if !ok { + mm = make(map[string]int) + m[path] = mm + } + mm[country]++ +} +add(hits, "/doc/", "au") +``` + +On the other hand, a design that uses a single map with a struct key does away with all that complexity: + +```go +type Key struct { + Path, Country string +} +hits := make(map[Key]int) +``` + +When a Vietnamese person visits the home page, incrementing (and possibly creating) the appropriate counter is a one-liner: + +```go +hits[Key{"/", "vn"}]++ +``` + +And it’s similarly straightforward to see how many Swiss people have read the spec: + +```go +n := hits[Key{"/ref/spec", "ch"}] +``` diff --git a/course/9-maps/exercises/3a-map_keys/multiple_choice.json b/course/9-maps/exercises/3a-map_keys/multiple_choice.json new file mode 100644 index 0000000..aeafc8a --- /dev/null +++ b/course/9-maps/exercises/3a-map_keys/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "Which is simpler?", + "answers": [ + "To use a struct directly as a key", + "To nest maps" + ] +} diff --git a/course/9-maps/exercises/3a-map_keys/readme.md b/course/9-maps/exercises/3a-map_keys/readme.md new file mode 100644 index 0000000..41317d4 --- /dev/null +++ b/course/9-maps/exercises/3a-map_keys/readme.md @@ -0,0 +1,54 @@ +# Key Types + +Any type can be used as the *value* in a map, but *keys* are more restrictive. + +Read the following section of the official [Go blog](https://go.dev/blog/maps): + +As mentioned earlier, **map keys may be of any type that is comparable**. The language spec defines this precisely, but in short, comparable types are boolean, numeric, string, pointer, channel, and interface types, and structs or arrays that contain only those types. Notably absent from the list are slices, maps, and functions; these types cannot be compared using ==, and may not be used as map keys. + +It's obvious that strings, ints, and other basic types should be available as map keys, but perhaps unexpected are struct keys. Struct can be used to key data by multiple dimensions. For example, this map of maps could be used to tally web page hits by country: + +```go +hits := make(map[string]map[string]int) +``` + +This is map of string to (map of string to int). Each key of the outer map is the path to a web page with its own inner map. Each inner map key is a two-letter country code. This expression retrieves the number of times an Australian has loaded the documentation page: + +```go +n := hits["/doc/"]["au"] +``` + +Unfortunately, this approach becomes unwieldy when adding data, as for any given outer key you must check if the inner map exists, and create it if needed: + +```go +func add(m map[string]map[string]int, path, country string) { + mm, ok := m[path] + if !ok { + mm = make(map[string]int) + m[path] = mm + } + mm[country]++ +} +add(hits, "/doc/", "au") +``` + +On the other hand, a design that uses a single map with a struct key does away with all that complexity: + +```go +type Key struct { + Path, Country string +} +hits := make(map[Key]int) +``` + +When a Vietnamese person visits the home page, incrementing (and possibly creating) the appropriate counter is a one-liner: + +```go +hits[Key{"/", "vn"}]++ +``` + +And it’s similarly straightforward to see how many Swiss people have read the spec: + +```go +n := hits[Key{"/ref/spec", "ch"}] +``` diff --git a/course/9-maps/exercises/4-maps_count/code.go b/course/9-maps/exercises/4-maps_count/code.go new file mode 100644 index 0000000..13f314b --- /dev/null +++ b/course/9-maps/exercises/4-maps_count/code.go @@ -0,0 +1,39 @@ +package main + +import ( + "crypto/md5" + "fmt" + "io" +) + +func getCounts(userIDs []string) map[string]int { + // ? +} + +// don't edit below this line + +func test(userIDs []string, ids []string) { + fmt.Printf("Generating counts for %v user IDs...\n", len(userIDs)) + + counts := getCounts(userIDs) + fmt.Println("Counts from select IDs:") + for _, k := range ids { + v := counts[k] + fmt.Printf(" - %s: %d\n", k, v) + } + fmt.Println("=====================================") +} + +func main() { + userIDs := []string{} + for i := 0; i < 10000; i++ { + h := md5.New() + io.WriteString(h, fmt.Sprint(i)) + key := fmt.Sprintf("%x", h.Sum(nil)) + userIDs = append(userIDs, key[:2]) + } + + test(userIDs, []string{"00", "ff", "dd"}) + test(userIDs, []string{"aa", "12", "32"}) + test(userIDs, []string{"bb", "33"}) +} diff --git a/course/9-maps/exercises/4-maps_count/complete.go b/course/9-maps/exercises/4-maps_count/complete.go new file mode 100644 index 0000000..1c51498 --- /dev/null +++ b/course/9-maps/exercises/4-maps_count/complete.go @@ -0,0 +1,46 @@ +package main + +import ( + "crypto/md5" + "fmt" + "io" +) + +func getCounts(userIDs []string) map[string]int { + m := map[string]int{} + for _, id := range userIDs { + if _, ok := m[id]; !ok { + m[id] = 0 + } + m[id]++ + } + return m +} + +// don't edit below this line + +func test(userIDs []string, ids []string) { + fmt.Printf("Generating counts for %v user IDs...\n", len(userIDs)) + + counts := getCounts(userIDs) + fmt.Println("Counts from select IDs:") + for _, k := range ids { + v := counts[k] + fmt.Printf(" - %s: %d\n", k, v) + } + fmt.Println("=====================================") +} + +func main() { + userIDs := []string{} + for i := 0; i < 10000; i++ { + h := md5.New() + io.WriteString(h, fmt.Sprint(i)) + key := fmt.Sprintf("%x", h.Sum(nil)) + userIDs = append(userIDs, key[:2]) + } + + test(userIDs, []string{"00", "ff", "dd"}) + test(userIDs, []string{"aa", "12", "32"}) + test(userIDs, []string{"bb", "33"}) +} diff --git a/course/9-maps/exercises/4-maps_count/expected.txt b/course/9-maps/exercises/4-maps_count/expected.txt new file mode 100644 index 0000000..15c2c89 --- /dev/null +++ b/course/9-maps/exercises/4-maps_count/expected.txt @@ -0,0 +1,17 @@ +Generating counts for 10000 user IDs... +Counts from select IDs: + - 00: 31 + - ff: 27 + - dd: 37 +===================================== +Generating counts for 10000 user IDs... +Counts from select IDs: + - aa: 29 + - 12: 29 + - 32: 41 +===================================== +Generating counts for 10000 user IDs... +Counts from select IDs: + - bb: 28 + - 33: 46 +===================================== diff --git a/course/9-maps/exercises/4-maps_count/readme.md b/course/9-maps/exercises/4-maps_count/readme.md new file mode 100644 index 0000000..36d7c65 --- /dev/null +++ b/course/9-maps/exercises/4-maps_count/readme.md @@ -0,0 +1,19 @@ +# Count Instances + +Remember that you can check if a key is already present in a map by using the second return value from the index operation. + +```go +names := map[string]int{} + +if _, ok := names["elon"]; !ok { + // if the key doesn't exist yet, + // initialize its value to 0 + names["elon"] = 0 +} +``` + +## Assignment + +We have a slice of user ids, and each instance of an id in the slice indicates that a message was sent to that user. We need to count up how many times each user's id appears in the slice to track how many messages they received. + +Implement the `getCounts` function. It should return a map of `string -> int` so that each `int` is a count of how many times each string was found in the slice. diff --git a/course/9-maps/exercises/5a-maps_quiz/multiple_choice.json b/course/9-maps/exercises/5a-maps_quiz/multiple_choice.json new file mode 100644 index 0000000..a77f9ae --- /dev/null +++ b/course/9-maps/exercises/5a-maps_quiz/multiple_choice.json @@ -0,0 +1,9 @@ +{ + "question": "Maps can have at most ____ value(s) associated with the same key", + "answers": [ + "1", + "2", + "3", + "any number of" + ] +} diff --git a/course/9-maps/exercises/5a-maps_quiz/readme.md b/course/9-maps/exercises/5a-maps_quiz/readme.md new file mode 100644 index 0000000..561f055 --- /dev/null +++ b/course/9-maps/exercises/5a-maps_quiz/readme.md @@ -0,0 +1,65 @@ +# Effective Go + +Read the following paraphrased sections from [effective Go regarding maps](https://go.dev/doc/effective_go#maps): + +## Like slices, maps hold references + +Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller. + +## Map literals + +Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization. + +```go +var timeZone = map[string]int{ + "UTC": 0*60*60, + "EST": -5*60*60, + "CST": -6*60*60, + "MST": -7*60*60, + "PST": -8*60*60, +} +``` + +## Missing keys + +An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing. + +```go +attended := map[string]bool{ + "Ann": true, + "Joe": true, + ... +} + +if attended[person] { // will be false if person is not in the map + fmt.Println(person, "was at the meeting") +} +``` + +Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that 0 because it's not in the map at all? You can discriminate with a form of multiple assignment. + +```go +var seconds int +var ok bool +seconds, ok = timeZone[tz] +``` + +For obvious reasons, this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report: + +```go +func offset(tz string) int { + if seconds, ok := timeZone[tz]; ok { + return seconds + } + log.Println("unknown time zone:", tz) + return 0 +} +``` + +## Deleting map entries + +To delete a map entry, use the delete built-in function, whose arguments are the map and the key to be deleted. It's safe to do this even if the key is already absent from the map. + +```go +delete(timeZone, "PDT") // Now on Standard Time +``` diff --git a/course/9-maps/exercises/5b-maps_quiz/multiple_choice.json b/course/9-maps/exercises/5b-maps_quiz/multiple_choice.json new file mode 100644 index 0000000..28a30af --- /dev/null +++ b/course/9-maps/exercises/5b-maps_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "Attempting to get a value from a map where the key doesn't exist...", + "answers": [ + "Returns the zero value", + "Panics", + "Returns the closest value" + ] +} diff --git a/course/9-maps/exercises/5b-maps_quiz/readme.md b/course/9-maps/exercises/5b-maps_quiz/readme.md new file mode 100644 index 0000000..561f055 --- /dev/null +++ b/course/9-maps/exercises/5b-maps_quiz/readme.md @@ -0,0 +1,65 @@ +# Effective Go + +Read the following paraphrased sections from [effective Go regarding maps](https://go.dev/doc/effective_go#maps): + +## Like slices, maps hold references + +Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller. + +## Map literals + +Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization. + +```go +var timeZone = map[string]int{ + "UTC": 0*60*60, + "EST": -5*60*60, + "CST": -6*60*60, + "MST": -7*60*60, + "PST": -8*60*60, +} +``` + +## Missing keys + +An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing. + +```go +attended := map[string]bool{ + "Ann": true, + "Joe": true, + ... +} + +if attended[person] { // will be false if person is not in the map + fmt.Println(person, "was at the meeting") +} +``` + +Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that 0 because it's not in the map at all? You can discriminate with a form of multiple assignment. + +```go +var seconds int +var ok bool +seconds, ok = timeZone[tz] +``` + +For obvious reasons, this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report: + +```go +func offset(tz string) int { + if seconds, ok := timeZone[tz]; ok { + return seconds + } + log.Println("unknown time zone:", tz) + return 0 +} +``` + +## Deleting map entries + +To delete a map entry, use the delete built-in function, whose arguments are the map and the key to be deleted. It's safe to do this even if the key is already absent from the map. + +```go +delete(timeZone, "PDT") // Now on Standard Time +``` diff --git a/course/9-maps/exercises/5c-maps_quiz/multiple_choice.json b/course/9-maps/exercises/5c-maps_quiz/multiple_choice.json new file mode 100644 index 0000000..82f8a63 --- /dev/null +++ b/course/9-maps/exercises/5c-maps_quiz/multiple_choice.json @@ -0,0 +1,7 @@ +{ + "question": "A function can mutate the values stored in a map and those changes ____ the caller", + "answers": [ + "affect", + "do not affect" + ] +} diff --git a/course/9-maps/exercises/5c-maps_quiz/readme.md b/course/9-maps/exercises/5c-maps_quiz/readme.md new file mode 100644 index 0000000..561f055 --- /dev/null +++ b/course/9-maps/exercises/5c-maps_quiz/readme.md @@ -0,0 +1,65 @@ +# Effective Go + +Read the following paraphrased sections from [effective Go regarding maps](https://go.dev/doc/effective_go#maps): + +## Like slices, maps hold references + +Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller. + +## Map literals + +Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization. + +```go +var timeZone = map[string]int{ + "UTC": 0*60*60, + "EST": -5*60*60, + "CST": -6*60*60, + "MST": -7*60*60, + "PST": -8*60*60, +} +``` + +## Missing keys + +An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing. + +```go +attended := map[string]bool{ + "Ann": true, + "Joe": true, + ... +} + +if attended[person] { // will be false if person is not in the map + fmt.Println(person, "was at the meeting") +} +``` + +Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that 0 because it's not in the map at all? You can discriminate with a form of multiple assignment. + +```go +var seconds int +var ok bool +seconds, ok = timeZone[tz] +``` + +For obvious reasons, this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report: + +```go +func offset(tz string) int { + if seconds, ok := timeZone[tz]; ok { + return seconds + } + log.Println("unknown time zone:", tz) + return 0 +} +``` + +## Deleting map entries + +To delete a map entry, use the delete built-in function, whose arguments are the map and the key to be deleted. It's safe to do this even if the key is already absent from the map. + +```go +delete(timeZone, "PDT") // Now on Standard Time +``` diff --git a/course/9-maps/exercises/5d-maps_quiz/multiple_choice.json b/course/9-maps/exercises/5d-maps_quiz/multiple_choice.json new file mode 100644 index 0000000..067613d --- /dev/null +++ b/course/9-maps/exercises/5d-maps_quiz/multiple_choice.json @@ -0,0 +1,8 @@ +{ + "question": "What does the second return value from a retrieve operation in a map indicate?", + "answers": [ + "A boolean that indicates whether the key exists", + "A boolean that indicates whether the value at the key is a nil value", + "A boolean that indicates whether the value at the key is a zero value" + ] +} diff --git a/course/9-maps/exercises/5d-maps_quiz/readme.md b/course/9-maps/exercises/5d-maps_quiz/readme.md new file mode 100644 index 0000000..561f055 --- /dev/null +++ b/course/9-maps/exercises/5d-maps_quiz/readme.md @@ -0,0 +1,65 @@ +# Effective Go + +Read the following paraphrased sections from [effective Go regarding maps](https://go.dev/doc/effective_go#maps): + +## Like slices, maps hold references + +Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller. + +## Map literals + +Maps can be constructed using the usual composite literal syntax with colon-separated key-value pairs, so it's easy to build them during initialization. + +```go +var timeZone = map[string]int{ + "UTC": 0*60*60, + "EST": -5*60*60, + "CST": -6*60*60, + "MST": -7*60*60, + "PST": -8*60*60, +} +``` + +## Missing keys + +An attempt to fetch a map value with a key that is not present in the map will return the zero value for the type of the entries in the map. For instance, if the map contains integers, looking up a non-existent key will return 0. A set can be implemented as a map with value type bool. Set the map entry to true to put the value in the set, and then test it by simple indexing. + +```go +attended := map[string]bool{ + "Ann": true, + "Joe": true, + ... +} + +if attended[person] { // will be false if person is not in the map + fmt.Println(person, "was at the meeting") +} +``` + +Sometimes you need to distinguish a missing entry from a zero value. Is there an entry for "UTC" or is that 0 because it's not in the map at all? You can discriminate with a form of multiple assignment. + +```go +var seconds int +var ok bool +seconds, ok = timeZone[tz] +``` + +For obvious reasons, this is called the “comma ok” idiom. In this example, if tz is present, seconds will be set appropriately and ok will be true; if not, seconds will be set to zero and ok will be false. Here's a function that puts it together with a nice error report: + +```go +func offset(tz string) int { + if seconds, ok := timeZone[tz]; ok { + return seconds + } + log.Println("unknown time zone:", tz) + return 0 +} +``` + +## Deleting map entries + +To delete a map entry, use the delete built-in function, whose arguments are the map and the key to be deleted. It's safe to do this even if the key is already absent from the map. + +```go +delete(timeZone, "PDT") // Now on Standard Time +``` diff --git a/course/9-maps/exercises/6-nested_maps/code.go b/course/9-maps/exercises/6-nested_maps/code.go new file mode 100644 index 0000000..9efe893 --- /dev/null +++ b/course/9-maps/exercises/6-nested_maps/code.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" +) + +func getNameCounts(names []string) map[rune]map[string]int { + // ? +} + +// don't edit below this line + +func test(names []string, initial rune, name string) { + fmt.Printf("Generating counts for %v names...\n", len(names)) + + nameCounts := getNameCounts(names) + count := nameCounts[initial][name] + fmt.Printf("Count for [%c][%s]: %d\n", initial, name, count) + fmt.Println("=====================================") +} + +func main() { + test(getNames(50), 'M', "Matthew") + test(getNames(100), 'G', "George") + test(getNames(150), 'D', "Drew") + test(getNames(200), 'P', "Philip") + test(getNames(250), 'B', "Bryant") + test(getNames(300), 'M', "Matthew") +} + +func getNames(length int) []string { + names := []string{ + "Grant", "Eduardo", "Peter", "Matthew", "Matthew", "Matthew", "Peter", "Peter", "Henry", "Parker", "Parker", "Parker", "Collin", "Hayden", "George", "Bradley", "Mitchell", "Devon", "Ricardo", "Shawn", "Taylor", "Nicolas", "Gregory", "Francisco", "Liam", "Kaleb", "Preston", "Erik", "Alexis", "Owen", "Omar", "Diego", "Dustin", "Corey", "Fernando", "Clayton", "Carter", "Ivan", "Jaden", "Javier", "Alec", "Johnathan", "Scott", "Manuel", "Cristian", "Alan", "Raymond", "Brett", "Max", "Drew", "Andres", "Gage", "Mario", "Dawson", "Dillon", "Cesar", "Wesley", "Levi", "Jakob", "Chandler", "Martin", "Malik", "Edgar", "Sergio", "Trenton", "Josiah", "Nolan", "Marco", "Drew", "Peyton", "Harrison", "Drew", "Hector", "Micah", "Roberto", "Drew", "Brady", "Erick", "Conner", "Jonah", "Casey", "Jayden", "Edwin", "Emmanuel", "Andre", "Phillip", "Brayden", "Landon", "Giovanni", "Bailey", "Ronald", "Braden", "Damian", "Donovan", "Ruben", "Frank", "Gerardo", "Pedro", "Andy", "Chance", "Abraham", "Calvin", "Trey", "Cade", "Donald", "Derrick", "Payton", "Darius", "Enrique", "Keith", "Raul", "Jaylen", "Troy", "Jonathon", "Cory", "Marc", "Eli", "Skyler", "Rafael", "Trent", "Griffin", "Colby", "Johnny", "Chad", "Armando", "Kobe", "Caden", "Marcos", "Cooper", "Elias", "Brenden", "Israel", "Avery", "Zane", "Zane", "Zane", "Zane", "Dante", "Josue", "Zackary", "Allen", "Philip", "Mathew", "Dennis", "Leonardo", "Ashton", "Philip", "Philip", "Philip", "Julio", "Miles", "Damien", "Ty", "Gustavo", "Drake", "Jaime", "Simon", "Jerry", "Curtis", "Kameron", "Lance", "Brock", "Bryson", "Alberto", "Dominick", "Jimmy", "Kaden", "Douglas", "Gary", "Brennan", "Zachery", "Randy", "Louis", "Larry", "Nickolas", "Albert", "Tony", "Fabian", "Keegan", "Saul", "Danny", "Tucker", "Myles", "Damon", "Arturo", "Corbin", "Deandre", "Ricky", "Kristopher", "Lane", "Pablo", "Darren", "Jarrett", "Zion", "Alfredo", "Micheal", "Angelo", "Carl", "Oliver", "Kyler", "Tommy", "Walter", "Dallas", "Jace", "Quinn", "Theodore", "Grayson", "Lorenzo", "Joe", "Arthur", "Bryant", "Roman", "Brent", "Russell", "Ramon", "Lawrence", "Moises", "Aiden", "Quentin", "Jay", "Tyrese", "Tristen", "Emanuel", "Salvador", "Terry", "Morgan", "Jeffery", "Esteban", "Tyson", "Braxton", "Branden", "Marvin", "Brody", "Craig", "Ismael", "Rodney", "Isiah", "Marshall", "Maurice", "Ernesto", "Emilio", "Brendon", "Kody", "Eddie", "Malachi", "Abel", "Keaton", "Jon", "Shaun", "Skylar", "Ezekiel", "Nikolas", "Santiago", "Kendall", "Axel", "Camden", "Trevon", "Bobby", "Conor", "Jamal", "Lukas", "Malcolm", "Zackery", "Jayson", "Javon", "Roger", "Reginald", "Zachariah", "Desmond", "Felix", "Johnathon", "Dean", "Quinton", "Ali", "Davis", "Gerald", "Rodrigo", "Demetrius", "Billy", "Rene", "Reece", "Kelvin", "Leo", "Justice", "Chris", "Guillermo", "Matthew", "Matthew", "Matthew", "Kevon", "Steve", "Frederick", "Clay", "Weston", "Dorian", "Hugo", "Roy", "Orlando", "Terrance", "Kai", "Khalil", "Khalil", "Khalil", "Graham", "Noel", "Willie", "Nathanael", "Terrell", "Tyrone", + } + if length > len(names) { + length = len(names) + } + return names[:length] +} diff --git a/course/9-maps/exercises/6-nested_maps/complete.go b/course/9-maps/exercises/6-nested_maps/complete.go new file mode 100644 index 0000000..6160712 --- /dev/null +++ b/course/9-maps/exercises/6-nested_maps/complete.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" +) + +func getNameCounts(names []string) map[rune]map[string]int { + nameCounts := make(map[rune]map[string]int) + for _, name := range names { + firstLetter := rune(0) + if len(name) != 0 { + firstLetter = rune(name[0]) + } + + if _, ok := nameCounts[firstLetter]; !ok { + nameCounts[firstLetter] = make(map[string]int) + } + if _, ok := nameCounts[firstLetter][name]; !ok { + nameCounts[firstLetter][name] = 0 + } + nameCounts[firstLetter][name]++ + } + return nameCounts +} + +// don't edit below this line + +func test(names []string, initial rune, name string) { + fmt.Printf("Generating counts for %v names...\n", len(names)) + + nameCounts := getNameCounts(names) + count := nameCounts[initial][name] + fmt.Printf("Count for [%c][%s]: %d\n", initial, name, count) + fmt.Println("=====================================") +} + +func main() { + test(getNames(50), 'M', "Matthew") + test(getNames(100), 'G', "George") + test(getNames(150), 'D', "Drew") + test(getNames(200), 'P', "Philip") + test(getNames(250), 'B', "Bryant") + test(getNames(300), 'M', "Matthew") +} + +func getNames(length int) []string { + names := []string{ + "Grant", "Eduardo", "Peter", "Matthew", "Matthew", "Matthew", "Peter", "Peter", "Henry", "Parker", "Parker", "Parker", "Collin", "Hayden", "George", "Bradley", "Mitchell", "Devon", "Ricardo", "Shawn", "Taylor", "Nicolas", "Gregory", "Francisco", "Liam", "Kaleb", "Preston", "Erik", "Alexis", "Owen", "Omar", "Diego", "Dustin", "Corey", "Fernando", "Clayton", "Carter", "Ivan", "Jaden", "Javier", "Alec", "Johnathan", "Scott", "Manuel", "Cristian", "Alan", "Raymond", "Brett", "Max", "Drew", "Andres", "Gage", "Mario", "Dawson", "Dillon", "Cesar", "Wesley", "Levi", "Jakob", "Chandler", "Martin", "Malik", "Edgar", "Sergio", "Trenton", "Josiah", "Nolan", "Marco", "Drew", "Peyton", "Harrison", "Drew", "Hector", "Micah", "Roberto", "Drew", "Brady", "Erick", "Conner", "Jonah", "Casey", "Jayden", "Edwin", "Emmanuel", "Andre", "Phillip", "Brayden", "Landon", "Giovanni", "Bailey", "Ronald", "Braden", "Damian", "Donovan", "Ruben", "Frank", "Gerardo", "Pedro", "Andy", "Chance", "Abraham", "Calvin", "Trey", "Cade", "Donald", "Derrick", "Payton", "Darius", "Enrique", "Keith", "Raul", "Jaylen", "Troy", "Jonathon", "Cory", "Marc", "Eli", "Skyler", "Rafael", "Trent", "Griffin", "Colby", "Johnny", "Chad", "Armando", "Kobe", "Caden", "Marcos", "Cooper", "Elias", "Brenden", "Israel", "Avery", "Zane", "Zane", "Zane", "Zane", "Dante", "Josue", "Zackary", "Allen", "Philip", "Mathew", "Dennis", "Leonardo", "Ashton", "Philip", "Philip", "Philip", "Julio", "Miles", "Damien", "Ty", "Gustavo", "Drake", "Jaime", "Simon", "Jerry", "Curtis", "Kameron", "Lance", "Brock", "Bryson", "Alberto", "Dominick", "Jimmy", "Kaden", "Douglas", "Gary", "Brennan", "Zachery", "Randy", "Louis", "Larry", "Nickolas", "Albert", "Tony", "Fabian", "Keegan", "Saul", "Danny", "Tucker", "Myles", "Damon", "Arturo", "Corbin", "Deandre", "Ricky", "Kristopher", "Lane", "Pablo", "Darren", "Jarrett", "Zion", "Alfredo", "Micheal", "Angelo", "Carl", "Oliver", "Kyler", "Tommy", "Walter", "Dallas", "Jace", "Quinn", "Theodore", "Grayson", "Lorenzo", "Joe", "Arthur", "Bryant", "Roman", "Brent", "Russell", "Ramon", "Lawrence", "Moises", "Aiden", "Quentin", "Jay", "Tyrese", "Tristen", "Emanuel", "Salvador", "Terry", "Morgan", "Jeffery", "Esteban", "Tyson", "Braxton", "Branden", "Marvin", "Brody", "Craig", "Ismael", "Rodney", "Isiah", "Marshall", "Maurice", "Ernesto", "Emilio", "Brendon", "Kody", "Eddie", "Malachi", "Abel", "Keaton", "Jon", "Shaun", "Skylar", "Ezekiel", "Nikolas", "Santiago", "Kendall", "Axel", "Camden", "Trevon", "Bobby", "Conor", "Jamal", "Lukas", "Malcolm", "Zackery", "Jayson", "Javon", "Roger", "Reginald", "Zachariah", "Desmond", "Felix", "Johnathon", "Dean", "Quinton", "Ali", "Davis", "Gerald", "Rodrigo", "Demetrius", "Billy", "Rene", "Reece", "Kelvin", "Leo", "Justice", "Chris", "Guillermo", "Matthew", "Matthew", "Matthew", "Kevon", "Steve", "Frederick", "Clay", "Weston", "Dorian", "Hugo", "Roy", "Orlando", "Terrance", "Kai", "Khalil", "Khalil", "Khalil", "Graham", "Noel", "Willie", "Nathanael", "Terrell", "Tyrone", + } + if length > len(names) { + length = len(names) + } + return names[:length] +} diff --git a/course/9-maps/exercises/6-nested_maps/expected.txt b/course/9-maps/exercises/6-nested_maps/expected.txt new file mode 100644 index 0000000..5b107e5 --- /dev/null +++ b/course/9-maps/exercises/6-nested_maps/expected.txt @@ -0,0 +1,18 @@ +Generating counts for 50 names... +Count for [M][Matthew]: 3 +===================================== +Generating counts for 100 names... +Count for [G][George]: 1 +===================================== +Generating counts for 150 names... +Count for [D][Drew]: 4 +===================================== +Generating counts for 200 names... +Count for [P][Philip]: 4 +===================================== +Generating counts for 250 names... +Count for [B][Bryant]: 1 +===================================== +Generating counts for 300 names... +Count for [M][Matthew]: 6 +===================================== diff --git a/course/9-maps/exercises/6-nested_maps/readme.md b/course/9-maps/exercises/6-nested_maps/readme.md new file mode 100644 index 0000000..a6b4372 --- /dev/null +++ b/course/9-maps/exercises/6-nested_maps/readme.md @@ -0,0 +1,38 @@ +# Nested + +Maps can contain maps, creating a nested structure. For example: + +```go +map[string]map[string]int +map[rune]map[string]int +map[int]map[string]map[string]int +``` + +## Assignment + +Because Textio is a glorified customer database, we have a lot of internal logic for sorting and dealing with customer names. + +Complete the `getNameCounts` function. It takes a slice of strings (names) and returns a nested map where the first key is all the unique first characters of the names, the second key is all the names themselves, and the value is the count of each name. + +For example: + +``` +billy +billy +bob +joe +``` + +Creates the following nested map: + +``` +b: { + billy: 2, + bob: 1 +}, +j: { + joe: 1 +} +``` + +Note that the test suite is *not* printing the map you're returning directly, but instead checking some specific keys. diff --git a/project/1-setup/readme.md b/project/1-setup/readme.md new file mode 100644 index 0000000..5072f2c --- /dev/null +++ b/project/1-setup/readme.md @@ -0,0 +1,33 @@ +# Welcome + +## What are we building? + +We're going to build an [RSS](https://en.wikipedia.org/wiki/RSS) feed aggregator in Go! It's a web server that allows clients to: + +* Add RSS feeds to be collected +* Follow and unfollow RSS feeds that other users have added +* Fetch all of the latest posts from the RSS feeds they follow + +RSS feeds are a way for websites to publish updates to their content. You can use this project to keep up with your favorite blogs, news sites, podcasts, and more! + +## Prerequisites + +This project assumes that you've already taken our "Learn Web Servers" course. If you haven't, go take it! It will give you a solid foundation for this project. + +## Learning goals + +* Learn how to integrate a Go server with PostgreSQL +* Learn about the basics of database migrations +* Learn about long-running service workers + +## Setup + +Before we dive into the project, let's make sure you have everything you'll need on your machine. + +1. An editor. I use [VS code](https://code.visualstudio.com/), you can use whatever you like. +2. A command line. I work on Mac OS/Linux, so instructions will be in Bash. I recommend [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install) if you're on Windows so you can still use Linux commands. +3. The latest [Go toolchain](https://golang.org/doc/install). +4. If you're in VS Code, I recommend the official [Go extension](https://marketplace.visualstudio.com/items?itemName=golang.Go). +5. An HTTP client. I use [Thunder Client](https://www.thunderclient.io/), but you can use whatever you like. + +If you're ready, move on to the next step! diff --git a/project/10-posts/readme.md b/project/10-posts/readme.md new file mode 100644 index 0000000..ef5b838 --- /dev/null +++ b/project/10-posts/readme.md @@ -0,0 +1,42 @@ +# Posts + +## Add a `posts` table to the database + +A post is a single entry from a feed. It should have: + +* `id` - a unique identifier for the post +* `created_at` - the time the record was created +* `updated_at` - the time the record was last updated +* `title` - the title of the post +* `url` - the URL of the post *this should be unique* +* `description` - the description of the post +* `published_at` - the time the post was published +* `feed_id` - the ID of the feed that the post came from + +Some of these fields can probably be null, others you might want to be more strict about - it's up to you. + +## Add a "create post" SQL query to the database + +This should insert a new post into the database. + +## Add a "get posts by user" SQL query to the database + +Order the results so that the most recent posts are first. Make the number of posts returned configurable. + +## Update your scraper to save posts + +Instead of just printing out the titles of the posts, save them to the database! If you encounter an error where the post with that URL already exists, just ignore it. That will happen a lot. If it's a different error, you should probably log it. + +Make sure that you're parsing the "published at" time properly from the feeds. Sometimes they might be in a different format than you expect, so you might need to handle that. + +## Add a "get posts by user" HTTP endpoint + +Endpoint: `GET /v1/posts` + +*This is an authenticated endpoint* + +This endpoint should return a list of posts for the authenticated user. It should accept a `limit` query parameter that limits the number of posts returned. The default if the parameter is not provided can be whatever you think is reasonable. + +## Start scraping some feeds! + +Test your scraper to make sure it's working! Go find some of your favorite websites and add their RSS feeds to your database. Then start your scraper and watch it go to work. diff --git a/project/10-posts/src/.gitignore b/project/10-posts/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/10-posts/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/10-posts/src/go.mod b/project/10-posts/src/go.mod new file mode 100644 index 0000000..0a0d083 --- /dev/null +++ b/project/10-posts/src/go.mod @@ -0,0 +1,11 @@ +module github.com/bootdotdev/projects/posts + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/google/uuid v1.3.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.7 +) diff --git a/project/10-posts/src/go.sum b/project/10-posts/src/go.sum new file mode 100644 index 0000000..cae7a15 --- /dev/null +++ b/project/10-posts/src/go.sum @@ -0,0 +1,10 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/project/10-posts/src/handler_feed.go b/project/10-posts/src/handler_feed.go new file mode 100644 index 0000000..028f56f --- /dev/null +++ b/project/10-posts/src/handler_feed.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/posts/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + Name string `json:"name"` + URL string `json:"url"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feed, err := cfg.DB.CreateFeed(r.Context(), database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + Name: params.Name, + Url: params.URL, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: feed.ID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct { + feed Feed + feedFollow FeedFollow + }{ + feed: databaseFeedToFeed(feed), + feedFollow: databaseFeedFollowToFeedFollow(feedFollow), + }) +} + +func (cfg *apiConfig) handlerGetFeeds(w http.ResponseWriter, r *http.Request) { + feeds, err := cfg.DB.GetFeeds(r.Context()) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get feeds") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedsToFeeds(feeds)) +} diff --git a/project/10-posts/src/handler_feed_follows.go b/project/10-posts/src/handler_feed_follows.go new file mode 100644 index 0000000..94d9dbd --- /dev/null +++ b/project/10-posts/src/handler_feed_follows.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/posts/internal/database" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedFollowsGet(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollows, err := cfg.DB.GetFeedFollowsForUser(r.Context(), user.ID) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowsToFeedFollows(feedFollows)) +} + +func (cfg *apiConfig) handlerFeedFollowCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + FeedID uuid.UUID + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: params.FeedID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowToFeedFollow(feedFollow)) +} + +func (cfg *apiConfig) handlerFeedFollowDelete(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollowIDStr := chi.URLParam(r, "feedFollowID") + feedFollowID, err := uuid.Parse(feedFollowIDStr) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid feed follow ID") + return + } + + err = cfg.DB.DeleteFeedFollow(r.Context(), database.DeleteFeedFollowParams{ + UserID: user.ID, + ID: feedFollowID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct{}{}) +} diff --git a/project/10-posts/src/handler_posts.go b/project/10-posts/src/handler_posts.go new file mode 100644 index 0000000..d3d5f11 --- /dev/null +++ b/project/10-posts/src/handler_posts.go @@ -0,0 +1,27 @@ +package main + +import ( + "net/http" + "strconv" + + "github.com/bootdotdev/projects/posts/internal/database" +) + +func (cfg *apiConfig) handlerPostsGet(w http.ResponseWriter, r *http.Request, user database.User) { + limitStr := r.URL.Query().Get("limit") + limit := 10 + if specifiedLimit, err := strconv.Atoi(limitStr); err == nil { + limit = specifiedLimit + } + + posts, err := cfg.DB.GetPostsForUser(r.Context(), database.GetPostsForUserParams{ + UserID: user.ID, + Limit: int32(limit), + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get posts for user") + return + } + + respondWithJSON(w, http.StatusOK, databasePostsToPosts(posts)) +} diff --git a/project/10-posts/src/handler_ready.go b/project/10-posts/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/10-posts/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/10-posts/src/handler_user.go b/project/10-posts/src/handler_user.go new file mode 100644 index 0000000..56b6ed5 --- /dev/null +++ b/project/10-posts/src/handler_user.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/posts/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/10-posts/src/internal/auth/auth.go b/project/10-posts/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/10-posts/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/10-posts/src/internal/database/db.go b/project/10-posts/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/10-posts/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/10-posts/src/internal/database/feed_follows.sql.go b/project/10-posts/src/internal/database/feed_follows.sql.go new file mode 100644 index 0000000..12ce1e8 --- /dev/null +++ b/project/10-posts/src/internal/database/feed_follows.sql.go @@ -0,0 +1,95 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feed_follows.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeedFollow = `-- name: CreateFeedFollow :one + +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, created_at, updated_at, user_id, feed_id +` + +type CreateFeedFollowParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (FeedFollow, error) { + row := q.db.QueryRowContext(ctx, createFeedFollow, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.UserID, + arg.FeedID, + ) + var i FeedFollow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ) + return i, err +} + +const deleteFeedFollow = `-- name: DeleteFeedFollow :exec + +DELETE FROM feed_follows WHERE id = $1 and user_id = $2 +` + +type DeleteFeedFollowParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error { + _, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.ID, arg.UserID) + return err +} + +const getFeedFollowsForUser = `-- name: GetFeedFollowsForUser :many +SELECT id, created_at, updated_at, user_id, feed_id FROM feed_follows WHERE user_id = $1 +` + +func (q *Queries) GetFeedFollowsForUser(ctx context.Context, userID uuid.UUID) ([]FeedFollow, error) { + rows, err := q.db.QueryContext(ctx, getFeedFollowsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FeedFollow + for rows.Next() { + var i FeedFollow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/10-posts/src/internal/database/feeds.sql.go b/project/10-posts/src/internal/database/feeds.sql.go new file mode 100644 index 0000000..449647c --- /dev/null +++ b/project/10-posts/src/internal/database/feeds.sql.go @@ -0,0 +1,145 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feeds.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at +` + +type CreateFeedParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + arg.Url, + arg.UserID, + ) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ) + return i, err +} + +const getFeeds = `-- name: GetFeeds :many +SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds +` + +func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getFeeds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getNextFeedsToFetch = `-- name: GetNextFeedsToFetch :many +SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds +ORDER BY last_fetched_at ASC NULLS FIRST +LIMIT $1 +` + +func (q *Queries) GetNextFeedsToFetch(ctx context.Context, limit int32) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getNextFeedsToFetch, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const markFeedFetched = `-- name: MarkFeedFetched :one +UPDATE feeds +SET last_fetched_at = NOW(), +updated_at = NOW() +WHERE id = $1 +RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at +` + +func (q *Queries) MarkFeedFetched(ctx context.Context, id uuid.UUID) (Feed, error) { + row := q.db.QueryRowContext(ctx, markFeedFetched, id) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ) + return i, err +} diff --git a/project/10-posts/src/internal/database/models.go b/project/10-posts/src/internal/database/models.go new file mode 100644 index 0000000..3721ac6 --- /dev/null +++ b/project/10-posts/src/internal/database/models.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +type Feed struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID + LastFetchedAt sql.NullTime +} + +type FeedFollow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +type Post struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Title string + Url string + Description sql.NullString + PublishedAt sql.NullTime + FeedID uuid.UUID +} + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/10-posts/src/internal/database/posts.sql.go b/project/10-posts/src/internal/database/posts.sql.go new file mode 100644 index 0000000..6cf94a9 --- /dev/null +++ b/project/10-posts/src/internal/database/posts.sql.go @@ -0,0 +1,102 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: posts.sql + +package database + +import ( + "context" + "database/sql" + "time" + + "github.com/google/uuid" +) + +const createPost = `-- name: CreatePost :one +INSERT INTO posts (id, created_at, updated_at, title, url, description, published_at, feed_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING id, created_at, updated_at, title, url, description, published_at, feed_id +` + +type CreatePostParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Title string + Url string + Description sql.NullString + PublishedAt sql.NullTime + FeedID uuid.UUID +} + +func (q *Queries) CreatePost(ctx context.Context, arg CreatePostParams) (Post, error) { + row := q.db.QueryRowContext(ctx, createPost, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Title, + arg.Url, + arg.Description, + arg.PublishedAt, + arg.FeedID, + ) + var i Post + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Title, + &i.Url, + &i.Description, + &i.PublishedAt, + &i.FeedID, + ) + return i, err +} + +const getPostsForUser = `-- name: GetPostsForUser :many + +SELECT posts.id, posts.created_at, posts.updated_at, posts.title, posts.url, posts.description, posts.published_at, posts.feed_id FROM posts +JOIN feed_follows ON feed_follows.feed_id = posts.feed_id +WHERE feed_follows.user_id = $1 +ORDER BY posts.published_at DESC +LIMIT $2 +` + +type GetPostsForUserParams struct { + UserID uuid.UUID + Limit int32 +} + +func (q *Queries) GetPostsForUser(ctx context.Context, arg GetPostsForUserParams) ([]Post, error) { + rows, err := q.db.QueryContext(ctx, getPostsForUser, arg.UserID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Post + for rows.Next() { + var i Post + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Title, + &i.Url, + &i.Description, + &i.PublishedAt, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/10-posts/src/internal/database/users.sql.go b/project/10-posts/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/10-posts/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/10-posts/src/json.go b/project/10-posts/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/10-posts/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/10-posts/src/main.go b/project/10-posts/src/main.go new file mode 100644 index 0000000..c28d994 --- /dev/null +++ b/project/10-posts/src/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/posts/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) + + v1Router.Post("/feeds", apiCfg.middlewareAuth(apiCfg.handlerFeedCreate)) + v1Router.Get("/feeds", apiCfg.handlerGetFeeds) + + v1Router.Get("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowsGet)) + v1Router.Post("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowCreate)) + v1Router.Delete("/feed_follows/{feedFollowID}", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowDelete)) + + v1Router.Get("/posts", apiCfg.middlewareAuth(apiCfg.handlerPostsGet)) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + const collectionConcurrency = 10 + const collectionInterval = time.Minute + go startScraping(dbQueries, collectionConcurrency, collectionInterval) + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/10-posts/src/middleware_auth.go b/project/10-posts/src/middleware_auth.go new file mode 100644 index 0000000..2b19b95 --- /dev/null +++ b/project/10-posts/src/middleware_auth.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/bootdotdev/projects/posts/internal/auth" + "github.com/bootdotdev/projects/posts/internal/database" +) + +type authedHandler func(http.ResponseWriter, *http.Request, database.User) + +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + handler(w, r, user) + } +} diff --git a/project/10-posts/src/models.go b/project/10-posts/src/models.go new file mode 100644 index 0000000..ad9bed1 --- /dev/null +++ b/project/10-posts/src/models.go @@ -0,0 +1,129 @@ +package main + +import ( + "database/sql" + "time" + + "github.com/bootdotdev/projects/posts/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} + +type Feed struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Url string `json:"url"` + UserID uuid.UUID `json:"user_id"` + LastFetchedAt *time.Time `json:"last_fetched_at"` +} + +func databaseFeedToFeed(feed database.Feed) Feed { + return Feed{ + ID: feed.ID, + CreatedAt: feed.CreatedAt, + UpdatedAt: feed.UpdatedAt, + Name: feed.Name, + Url: feed.Url, + UserID: feed.UserID, + LastFetchedAt: nullTimeToTimePtr(feed.LastFetchedAt), + } +} + +func databaseFeedsToFeeds(feeds []database.Feed) []Feed { + result := make([]Feed, len(feeds)) + for i, feed := range feeds { + result[i] = databaseFeedToFeed(feed) + } + return result +} + +type FeedFollow struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID uuid.UUID `json:"user_id"` + FeedID uuid.UUID `json:"feed_id"` +} + +func databaseFeedFollowToFeedFollow(feedFollow database.FeedFollow) FeedFollow { + return FeedFollow{ + ID: feedFollow.ID, + CreatedAt: feedFollow.CreatedAt, + UpdatedAt: feedFollow.UpdatedAt, + UserID: feedFollow.UserID, + FeedID: feedFollow.FeedID, + } +} + +func databaseFeedFollowsToFeedFollows(feedFollows []database.FeedFollow) []FeedFollow { + result := make([]FeedFollow, len(feedFollows)) + for i, feedFollow := range feedFollows { + result[i] = databaseFeedFollowToFeedFollow(feedFollow) + } + return result +} + +type Post struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Title string `json:"title"` + Url string `json:"url"` + Description *string `json:"description"` + PublishedAt *time.Time `json:"published_at"` + FeedID uuid.UUID `json:"feed_id"` +} + +func databasePostToPost(post database.Post) Post { + return Post{ + ID: post.ID, + CreatedAt: post.CreatedAt, + UpdatedAt: post.UpdatedAt, + Title: post.Title, + Url: post.Url, + Description: nullStringToStringPtr(post.Description), + PublishedAt: nullTimeToTimePtr(post.PublishedAt), + FeedID: post.FeedID, + } +} + +func databasePostsToPosts(posts []database.Post) []Post { + result := make([]Post, len(posts)) + for i, post := range posts { + result[i] = databasePostToPost(post) + } + return result +} + +func nullTimeToTimePtr(t sql.NullTime) *time.Time { + if t.Valid { + return &t.Time + } + return nil +} + +func nullStringToStringPtr(s sql.NullString) *string { + if s.Valid { + return &s.String + } + return nil +} diff --git a/project/10-posts/src/scraper.go b/project/10-posts/src/scraper.go new file mode 100644 index 0000000..5777cab --- /dev/null +++ b/project/10-posts/src/scraper.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "database/sql" + "encoding/xml" + "io" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/bootdotdev/projects/posts/internal/database" + "github.com/google/uuid" +) + +func startScraping(db *database.Queries, concurrency int, timeBetweenRequest time.Duration) { + log.Printf("Collecting feeds every %s on %v goroutines...", timeBetweenRequest, concurrency) + ticker := time.NewTicker(timeBetweenRequest) + + for ; ; <-ticker.C { + feeds, err := db.GetNextFeedsToFetch(context.Background(), int32(concurrency)) + if err != nil { + log.Println("Couldn't get next feeds to fetch", err) + continue + } + log.Printf("Found %v feeds to fetch!", len(feeds)) + + wg := &sync.WaitGroup{} + for _, feed := range feeds { + wg.Add(1) + go scrapeFeed(db, wg, feed) + } + wg.Wait() + } +} + +func scrapeFeed(db *database.Queries, wg *sync.WaitGroup, feed database.Feed) { + defer wg.Done() + _, err := db.MarkFeedFetched(context.Background(), feed.ID) + if err != nil { + log.Printf("Couldn't mark feed %s fetched: %v", feed.Name, err) + return + } + + feedData, err := fetchFeed(feed.Url) + if err != nil { + log.Printf("Couldn't collect feed %s: %v", feed.Name, err) + return + } + for _, item := range feedData.Channel.Item { + publishedAt := sql.NullTime{} + if t, err := time.Parse(time.RFC1123Z, item.PubDate); err == nil { + publishedAt = sql.NullTime{ + Time: t, + Valid: true, + } + } + + _, err = db.CreatePost(context.Background(), database.CreatePostParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + FeedID: feed.ID, + Title: item.Title, + Description: sql.NullString{ + String: item.Description, + Valid: true, + }, + Url: item.Link, + PublishedAt: publishedAt, + }) + if err != nil { + if strings.Contains(err.Error(), "duplicate key value violates unique constraint") { + continue + } + log.Printf("Couldn't create post: %v", err) + continue + } + } + log.Printf("Feed %s collected, %v posts found", feed.Name, len(feedData.Channel.Item)) +} + +type RSSFeed struct { + Channel struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Language string `xml:"language"` + Item []RSSItem `xml:"item"` + } `xml:"channel"` +} + +type RSSItem struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate string `xml:"pubDate"` +} + +func fetchFeed(feedURL string) (*RSSFeed, error) { + httpClient := http.Client{ + Timeout: 10 * time.Second, + } + resp, err := httpClient.Get(feedURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + dat, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var rssFeed RSSFeed + err = xml.Unmarshal(dat, &rssFeed) + if err != nil { + return nil, err + } + + return &rssFeed, nil +} diff --git a/project/10-posts/src/sql/queries/feed_follows.sql b/project/10-posts/src/sql/queries/feed_follows.sql new file mode 100644 index 0000000..ebef9a9 --- /dev/null +++ b/project/10-posts/src/sql/queries/feed_follows.sql @@ -0,0 +1,13 @@ +-- name: GetFeedFollowsForUser :many +SELECT * FROM feed_follows WHERE user_id = $1; +-- + +-- name: CreateFeedFollow :one +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING *; +-- + +-- name: DeleteFeedFollow :exec +DELETE FROM feed_follows WHERE id = $1 and user_id = $2; +-- diff --git a/project/10-posts/src/sql/queries/feeds.sql b/project/10-posts/src/sql/queries/feeds.sql new file mode 100644 index 0000000..7d243fc --- /dev/null +++ b/project/10-posts/src/sql/queries/feeds.sql @@ -0,0 +1,19 @@ +-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: GetFeeds :many +SELECT * FROM feeds; + +-- name: GetNextFeedsToFetch :many +SELECT * FROM feeds +ORDER BY last_fetched_at ASC NULLS FIRST +LIMIT $1; + +-- name: MarkFeedFetched :one +UPDATE feeds +SET last_fetched_at = NOW(), +updated_at = NOW() +WHERE id = $1 +RETURNING *; diff --git a/project/10-posts/src/sql/queries/posts.sql b/project/10-posts/src/sql/queries/posts.sql new file mode 100644 index 0000000..1509495 --- /dev/null +++ b/project/10-posts/src/sql/queries/posts.sql @@ -0,0 +1,13 @@ +-- name: CreatePost :one +INSERT INTO posts (id, created_at, updated_at, title, url, description, published_at, feed_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING *; +-- + +-- name: GetPostsForUser :many +SELECT posts.* FROM posts +JOIN feed_follows ON feed_follows.feed_id = posts.feed_id +WHERE feed_follows.user_id = $1 +ORDER BY posts.published_at DESC +LIMIT $2; +-- diff --git a/project/10-posts/src/sql/queries/users.sql b/project/10-posts/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/10-posts/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/10-posts/src/sql/schema/001_users.sql b/project/10-posts/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/10-posts/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/10-posts/src/sql/schema/002_users_apikey.sql b/project/10-posts/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/10-posts/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/10-posts/src/sql/schema/003_feeds.sql b/project/10-posts/src/sql/schema/003_feeds.sql new file mode 100644 index 0000000..8c9f831 --- /dev/null +++ b/project/10-posts/src/sql/schema/003_feeds.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feeds ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE feeds; diff --git a/project/10-posts/src/sql/schema/004_feed_follows.sql b/project/10-posts/src/sql/schema/004_feed_follows.sql new file mode 100644 index 0000000..f5e108b --- /dev/null +++ b/project/10-posts/src/sql/schema/004_feed_follows.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feed_follows ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + feed_id UUID NOT NULL REFERENCES feeds(id) ON DELETE CASCADE, + UNIQUE (user_id, feed_id) +); + +-- +goose Down +DROP TABLE feed_follows; diff --git a/project/10-posts/src/sql/schema/005_feed_lastfetched.sql b/project/10-posts/src/sql/schema/005_feed_lastfetched.sql new file mode 100644 index 0000000..6a38099 --- /dev/null +++ b/project/10-posts/src/sql/schema/005_feed_lastfetched.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE feeds ADD COLUMN last_fetched_at TIMESTAMP; + +-- +goose Down +ALTER TABLE feeds DROP COLUMN last_fetched_at; diff --git a/project/10-posts/src/sql/schema/006_posts.sql b/project/10-posts/src/sql/schema/006_posts.sql new file mode 100644 index 0000000..27d4e0b --- /dev/null +++ b/project/10-posts/src/sql/schema/006_posts.sql @@ -0,0 +1,14 @@ +-- +goose Up +CREATE TABLE posts ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + title TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + description TEXT, + published_at TIMESTAMP, + feed_id UUID NOT NULL REFERENCES feeds(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE posts; diff --git a/project/10-posts/src/sqlc.yaml b/project/10-posts/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/10-posts/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/.gitignore b/project/10-posts/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/10-posts/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/10-posts/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/LICENSE b/project/10-posts/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/Makefile b/project/10-posts/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/README.md b/project/10-posts/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/chain.go b/project/10-posts/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/chi.go b/project/10-posts/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/context.go b/project/10-posts/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/mux.go b/project/10-posts/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/10-posts/src/vendor/github.com/go-chi/chi/tree.go b/project/10-posts/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/10-posts/src/vendor/github.com/go-chi/cors/LICENSE b/project/10-posts/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/10-posts/src/vendor/github.com/go-chi/cors/README.md b/project/10-posts/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/10-posts/src/vendor/github.com/go-chi/cors/cors.go b/project/10-posts/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/10-posts/src/vendor/github.com/go-chi/cors/utils.go b/project/10-posts/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/10-posts/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/.travis.yml b/project/10-posts/src/vendor/github.com/google/uuid/.travis.yml new file mode 100644 index 0000000..d8156a6 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.4.3 + - 1.5.3 + - tip + +script: + - go test -v ./... diff --git a/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTING.md b/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTING.md new file mode 100644 index 0000000..04fdf09 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# How to contribute + +We definitely welcome patches and contribution to this project! + +### Legal requirements + +In order to protect both you and ourselves, you will need to sign the +[Contributor License Agreement](https://cla.developers.google.com/clas). + +You may have already signed it for other Google projects. diff --git a/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTORS b/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTORS new file mode 100644 index 0000000..b4bb97f --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/CONTRIBUTORS @@ -0,0 +1,9 @@ +Paul Borman +bmatsuo +shawnps +theory +jboverfelt +dsymonds +cd1 +wallclockbuilder +dansouza diff --git a/project/10-posts/src/vendor/github.com/google/uuid/LICENSE b/project/10-posts/src/vendor/github.com/google/uuid/LICENSE new file mode 100644 index 0000000..5dc6826 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009,2014 Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/project/10-posts/src/vendor/github.com/google/uuid/README.md b/project/10-posts/src/vendor/github.com/google/uuid/README.md new file mode 100644 index 0000000..f765a46 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/README.md @@ -0,0 +1,19 @@ +# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) +The uuid package generates and inspects UUIDs based on +[RFC 4122](http://tools.ietf.org/html/rfc4122) +and DCE 1.1: Authentication and Security Services. + +This package is based on the github.com/pborman/uuid package (previously named +code.google.com/p/go-uuid). It differs from these earlier packages in that +a UUID is a 16 byte array rather than a byte slice. One loss due to this +change is the ability to represent an invalid UUID (vs a NIL UUID). + +###### Install +`go get github.com/google/uuid` + +###### Documentation +[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) + +Full `go doc` style documentation for the package can be viewed online without +installing this package by using the GoDoc site here: +http://pkg.go.dev/github.com/google/uuid diff --git a/project/10-posts/src/vendor/github.com/google/uuid/dce.go b/project/10-posts/src/vendor/github.com/google/uuid/dce.go new file mode 100644 index 0000000..fa820b9 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/dce.go @@ -0,0 +1,80 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "fmt" + "os" +) + +// A Domain represents a Version 2 domain +type Domain byte + +// Domain constants for DCE Security (Version 2) UUIDs. +const ( + Person = Domain(0) + Group = Domain(1) + Org = Domain(2) +) + +// NewDCESecurity returns a DCE Security (Version 2) UUID. +// +// The domain should be one of Person, Group or Org. +// On a POSIX system the id should be the users UID for the Person +// domain and the users GID for the Group. The meaning of id for +// the domain Org or on non-POSIX systems is site defined. +// +// For a given domain/id pair the same token may be returned for up to +// 7 minutes and 10 seconds. +func NewDCESecurity(domain Domain, id uint32) (UUID, error) { + uuid, err := NewUUID() + if err == nil { + uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 + uuid[9] = byte(domain) + binary.BigEndian.PutUint32(uuid[0:], id) + } + return uuid, err +} + +// NewDCEPerson returns a DCE Security (Version 2) UUID in the person +// domain with the id returned by os.Getuid. +// +// NewDCESecurity(Person, uint32(os.Getuid())) +func NewDCEPerson() (UUID, error) { + return NewDCESecurity(Person, uint32(os.Getuid())) +} + +// NewDCEGroup returns a DCE Security (Version 2) UUID in the group +// domain with the id returned by os.Getgid. +// +// NewDCESecurity(Group, uint32(os.Getgid())) +func NewDCEGroup() (UUID, error) { + return NewDCESecurity(Group, uint32(os.Getgid())) +} + +// Domain returns the domain for a Version 2 UUID. Domains are only defined +// for Version 2 UUIDs. +func (uuid UUID) Domain() Domain { + return Domain(uuid[9]) +} + +// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 +// UUIDs. +func (uuid UUID) ID() uint32 { + return binary.BigEndian.Uint32(uuid[0:4]) +} + +func (d Domain) String() string { + switch d { + case Person: + return "Person" + case Group: + return "Group" + case Org: + return "Org" + } + return fmt.Sprintf("Domain%d", int(d)) +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/doc.go b/project/10-posts/src/vendor/github.com/google/uuid/doc.go new file mode 100644 index 0000000..5b8a4b9 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/doc.go @@ -0,0 +1,12 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package uuid generates and inspects UUIDs. +// +// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security +// Services. +// +// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to +// maps or compared directly. +package uuid diff --git a/project/10-posts/src/vendor/github.com/google/uuid/hash.go b/project/10-posts/src/vendor/github.com/google/uuid/hash.go new file mode 100644 index 0000000..b404f4b --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/hash.go @@ -0,0 +1,53 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "crypto/md5" + "crypto/sha1" + "hash" +) + +// Well known namespace IDs and UUIDs +var ( + NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) + Nil UUID // empty UUID, all zeros +) + +// NewHash returns a new UUID derived from the hash of space concatenated with +// data generated by h. The hash should be at least 16 byte in length. The +// first 16 bytes of the hash are used to form the UUID. The version of the +// UUID will be the lower 4 bits of version. NewHash is used to implement +// NewMD5 and NewSHA1. +func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { + h.Reset() + h.Write(space[:]) //nolint:errcheck + h.Write(data) //nolint:errcheck + s := h.Sum(nil) + var uuid UUID + copy(uuid[:], s) + uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) + uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant + return uuid +} + +// NewMD5 returns a new MD5 (Version 3) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(md5.New(), space, data, 3) +func NewMD5(space UUID, data []byte) UUID { + return NewHash(md5.New(), space, data, 3) +} + +// NewSHA1 returns a new SHA1 (Version 5) UUID based on the +// supplied name space and data. It is the same as calling: +// +// NewHash(sha1.New(), space, data, 5) +func NewSHA1(space UUID, data []byte) UUID { + return NewHash(sha1.New(), space, data, 5) +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/marshal.go b/project/10-posts/src/vendor/github.com/google/uuid/marshal.go new file mode 100644 index 0000000..14bd340 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/marshal.go @@ -0,0 +1,38 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "fmt" + +// MarshalText implements encoding.TextMarshaler. +func (uuid UUID) MarshalText() ([]byte, error) { + var js [36]byte + encodeHex(js[:], uuid) + return js[:], nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (uuid *UUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + return err + } + *uuid = id + return nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (uuid UUID) MarshalBinary() ([]byte, error) { + return uuid[:], nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (uuid *UUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(uuid[:], data) + return nil +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/node.go b/project/10-posts/src/vendor/github.com/google/uuid/node.go new file mode 100644 index 0000000..d651a2b --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/node.go @@ -0,0 +1,90 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "sync" +) + +var ( + nodeMu sync.Mutex + ifname string // name of interface being used + nodeID [6]byte // hardware for version 1 UUIDs + zeroID [6]byte // nodeID with only 0's +) + +// NodeInterface returns the name of the interface from which the NodeID was +// derived. The interface "user" is returned if the NodeID was set by +// SetNodeID. +func NodeInterface() string { + defer nodeMu.Unlock() + nodeMu.Lock() + return ifname +} + +// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. +// If name is "" then the first usable interface found will be used or a random +// Node ID will be generated. If a named interface cannot be found then false +// is returned. +// +// SetNodeInterface never fails when name is "". +func SetNodeInterface(name string) bool { + defer nodeMu.Unlock() + nodeMu.Lock() + return setNodeInterface(name) +} + +func setNodeInterface(name string) bool { + iname, addr := getHardwareInterface(name) // null implementation for js + if iname != "" && addr != nil { + ifname = iname + copy(nodeID[:], addr) + return true + } + + // We found no interfaces with a valid hardware address. If name + // does not specify a specific interface generate a random Node ID + // (section 4.1.6) + if name == "" { + ifname = "random" + randomBits(nodeID[:]) + return true + } + return false +} + +// NodeID returns a slice of a copy of the current Node ID, setting the Node ID +// if not already set. +func NodeID() []byte { + defer nodeMu.Unlock() + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + nid := nodeID + return nid[:] +} + +// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes +// of id are used. If id is less than 6 bytes then false is returned and the +// Node ID is not set. +func SetNodeID(id []byte) bool { + if len(id) < 6 { + return false + } + defer nodeMu.Unlock() + nodeMu.Lock() + copy(nodeID[:], id) + ifname = "user" + return true +} + +// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is +// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) NodeID() []byte { + var node [6]byte + copy(node[:], uuid[10:]) + return node[:] +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/node_js.go b/project/10-posts/src/vendor/github.com/google/uuid/node_js.go new file mode 100644 index 0000000..24b78ed --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/node_js.go @@ -0,0 +1,12 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build js + +package uuid + +// getHardwareInterface returns nil values for the JS version of the code. +// This remvoves the "net" dependency, because it is not used in the browser. +// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. +func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/project/10-posts/src/vendor/github.com/google/uuid/node_net.go b/project/10-posts/src/vendor/github.com/google/uuid/node_net.go new file mode 100644 index 0000000..0cbbcdd --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/node_net.go @@ -0,0 +1,33 @@ +// Copyright 2017 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !js + +package uuid + +import "net" + +var interfaces []net.Interface // cached list of interfaces + +// getHardwareInterface returns the name and hardware address of interface name. +// If name is "" then the name and hardware address of one of the system's +// interfaces is returned. If no interfaces are found (name does not exist or +// there are no interfaces) then "", nil is returned. +// +// Only addresses of at least 6 bytes are returned. +func getHardwareInterface(name string) (string, []byte) { + if interfaces == nil { + var err error + interfaces, err = net.Interfaces() + if err != nil { + return "", nil + } + } + for _, ifs := range interfaces { + if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { + return ifs.Name, ifs.HardwareAddr + } + } + return "", nil +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/null.go b/project/10-posts/src/vendor/github.com/google/uuid/null.go new file mode 100644 index 0000000..d7fcbf2 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/null.go @@ -0,0 +1,118 @@ +// Copyright 2021 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" +) + +var jsonNull = []byte("null") + +// NullUUID represents a UUID that may be null. +// NullUUID implements the SQL driver.Scanner interface so +// it can be used as a scan destination: +// +// var u uuid.NullUUID +// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u) +// ... +// if u.Valid { +// // use u.UUID +// } else { +// // NULL value +// } +// +type NullUUID struct { + UUID UUID + Valid bool // Valid is true if UUID is not NULL +} + +// Scan implements the SQL driver.Scanner interface. +func (nu *NullUUID) Scan(value interface{}) error { + if value == nil { + nu.UUID, nu.Valid = Nil, false + return nil + } + + err := nu.UUID.Scan(value) + if err != nil { + nu.Valid = false + return err + } + + nu.Valid = true + return nil +} + +// Value implements the driver Valuer interface. +func (nu NullUUID) Value() (driver.Value, error) { + if !nu.Valid { + return nil, nil + } + // Delegate to UUID Value function + return nu.UUID.Value() +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (nu NullUUID) MarshalBinary() ([]byte, error) { + if nu.Valid { + return nu.UUID[:], nil + } + + return []byte(nil), nil +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (nu *NullUUID) UnmarshalBinary(data []byte) error { + if len(data) != 16 { + return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) + } + copy(nu.UUID[:], data) + nu.Valid = true + return nil +} + +// MarshalText implements encoding.TextMarshaler. +func (nu NullUUID) MarshalText() ([]byte, error) { + if nu.Valid { + return nu.UUID.MarshalText() + } + + return jsonNull, nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (nu *NullUUID) UnmarshalText(data []byte) error { + id, err := ParseBytes(data) + if err != nil { + nu.Valid = false + return err + } + nu.UUID = id + nu.Valid = true + return nil +} + +// MarshalJSON implements json.Marshaler. +func (nu NullUUID) MarshalJSON() ([]byte, error) { + if nu.Valid { + return json.Marshal(nu.UUID) + } + + return jsonNull, nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (nu *NullUUID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, jsonNull) { + *nu = NullUUID{} + return nil // valid null UUID + } + err := json.Unmarshal(data, &nu.UUID) + nu.Valid = err == nil + return err +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/sql.go b/project/10-posts/src/vendor/github.com/google/uuid/sql.go new file mode 100644 index 0000000..2e02ec0 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/sql.go @@ -0,0 +1,59 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements sql.Scanner so UUIDs can be read from databases transparently. +// Currently, database types that map to string and []byte are supported. Please +// consult database-specific driver documentation for matching types. +func (uuid *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + return nil + + case string: + // if an empty UUID comes from a table, we return a null UUID + if src == "" { + return nil + } + + // see Parse for required string format + u, err := Parse(src) + if err != nil { + return fmt.Errorf("Scan: %v", err) + } + + *uuid = u + + case []byte: + // if an empty UUID comes from a table, we return a null UUID + if len(src) == 0 { + return nil + } + + // assumes a simple slice of bytes if 16 bytes + // otherwise attempts to parse + if len(src) != 16 { + return uuid.Scan(string(src)) + } + copy((*uuid)[:], src) + + default: + return fmt.Errorf("Scan: unable to scan type %T into UUID", src) + } + + return nil +} + +// Value implements sql.Valuer so that UUIDs can be written to databases +// transparently. Currently, UUIDs map to strings. Please consult +// database-specific driver documentation for matching types. +func (uuid UUID) Value() (driver.Value, error) { + return uuid.String(), nil +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/time.go b/project/10-posts/src/vendor/github.com/google/uuid/time.go new file mode 100644 index 0000000..e6ef06c --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/time.go @@ -0,0 +1,123 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "sync" + "time" +) + +// A Time represents a time as the number of 100's of nanoseconds since 15 Oct +// 1582. +type Time int64 + +const ( + lillian = 2299160 // Julian day of 15 Oct 1582 + unix = 2440587 // Julian day of 1 Jan 1970 + epoch = unix - lillian // Days between epochs + g1582 = epoch * 86400 // seconds between epochs + g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs +) + +var ( + timeMu sync.Mutex + lasttime uint64 // last time we returned + clockSeq uint16 // clock sequence for this run + + timeNow = time.Now // for testing +) + +// UnixTime converts t the number of seconds and nanoseconds using the Unix +// epoch of 1 Jan 1970. +func (t Time) UnixTime() (sec, nsec int64) { + sec = int64(t - g1582ns100) + nsec = (sec % 10000000) * 100 + sec /= 10000000 + return sec, nsec +} + +// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and +// clock sequence as well as adjusting the clock sequence as needed. An error +// is returned if the current time cannot be determined. +func GetTime() (Time, uint16, error) { + defer timeMu.Unlock() + timeMu.Lock() + return getTime() +} + +func getTime() (Time, uint16, error) { + t := timeNow() + + // If we don't have a clock sequence already, set one. + if clockSeq == 0 { + setClockSequence(-1) + } + now := uint64(t.UnixNano()/100) + g1582ns100 + + // If time has gone backwards with this clock sequence then we + // increment the clock sequence + if now <= lasttime { + clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 + } + lasttime = now + return Time(now), clockSeq, nil +} + +// ClockSequence returns the current clock sequence, generating one if not +// already set. The clock sequence is only used for Version 1 UUIDs. +// +// The uuid package does not use global static storage for the clock sequence or +// the last time a UUID was generated. Unless SetClockSequence is used, a new +// random clock sequence is generated the first time a clock sequence is +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +func ClockSequence() int { + defer timeMu.Unlock() + timeMu.Lock() + return clockSequence() +} + +func clockSequence() int { + if clockSeq == 0 { + setClockSequence(-1) + } + return int(clockSeq & 0x3fff) +} + +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. +func SetClockSequence(seq int) { + defer timeMu.Unlock() + timeMu.Lock() + setClockSequence(seq) +} + +func setClockSequence(seq int) { + if seq == -1 { + var b [2]byte + randomBits(b[:]) // clock sequence + seq = int(b[0])<<8 | int(b[1]) + } + oldSeq := clockSeq + clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant + if oldSeq != clockSeq { + lasttime = 0 + } +} + +// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in +// uuid. The time is only defined for version 1 and 2 UUIDs. +func (uuid UUID) Time() Time { + time := int64(binary.BigEndian.Uint32(uuid[0:4])) + time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 + time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 + return Time(time) +} + +// ClockSequence returns the clock sequence encoded in uuid. +// The clock sequence is only well defined for version 1 and 2 UUIDs. +func (uuid UUID) ClockSequence() int { + return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/util.go b/project/10-posts/src/vendor/github.com/google/uuid/util.go new file mode 100644 index 0000000..5ea6c73 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/util.go @@ -0,0 +1,43 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "io" +) + +// randomBits completely fills slice b with random data. +func randomBits(b []byte) { + if _, err := io.ReadFull(rander, b); err != nil { + panic(err.Error()) // rand should never fail + } +} + +// xvalues returns the value of a byte as a hexadecimal digit or 255. +var xvalues = [256]byte{ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, +} + +// xtob converts hex characters x1 and x2 into a byte. +func xtob(x1, x2 byte) (byte, bool) { + b1 := xvalues[x1] + b2 := xvalues[x2] + return (b1 << 4) | b2, b1 != 255 && b2 != 255 +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/uuid.go b/project/10-posts/src/vendor/github.com/google/uuid/uuid.go new file mode 100644 index 0000000..a57207a --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/uuid.go @@ -0,0 +1,294 @@ +// Copyright 2018 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + "sync" +) + +// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC +// 4122. +type UUID [16]byte + +// A Version represents a UUID's version. +type Version byte + +// A Variant represents a UUID's variant. +type Variant byte + +// Constants returned by Variant. +const ( + Invalid = Variant(iota) // Invalid UUID + RFC4122 // The variant specified in RFC4122 + Reserved // Reserved, NCS backward compatibility. + Microsoft // Reserved, Microsoft Corporation backward compatibility. + Future // Reserved for future definition. +) + +const randPoolSize = 16 * 16 + +var ( + rander = rand.Reader // random function + poolEnabled = false + poolMu sync.Mutex + poolPos = randPoolSize // protected with poolMu + pool [randPoolSize]byte // protected with poolMu +) + +type invalidLengthError struct{ len int } + +func (err invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + +// IsInvalidLengthError is matcher function for custom error invalidLengthError +func IsInvalidLengthError(err error) bool { + _, ok := err.(invalidLengthError) + return ok +} + +// Parse decodes s into a UUID or returns an error. Both the standard UUID +// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the +// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex +// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. +func Parse(s string) (UUID, error) { + var uuid UUID + switch len(s) { + // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36: + + // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: + if strings.ToLower(s[:9]) != "urn:uuid:" { + return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) + } + s = s[9:] + + // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + case 36 + 2: + s = s[1:] + + // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + case 32: + var ok bool + for i := range uuid { + uuid[i], ok = xtob(s[i*2], s[i*2+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(s)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(s[x], s[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// ParseBytes is like Parse, except it parses a byte slice instead of a string. +func ParseBytes(b []byte) (UUID, error) { + var uuid UUID + switch len(b) { + case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { + return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) + } + b = b[9:] + case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} + b = b[1:] + case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + var ok bool + for i := 0; i < 32; i += 2 { + uuid[i/2], ok = xtob(b[i], b[i+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + } + return uuid, nil + default: + return uuid, invalidLengthError{len(b)} + } + // s is now at least 36 bytes long + // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return uuid, errors.New("invalid UUID format") + } + for i, x := range [16]int{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34} { + v, ok := xtob(b[x], b[x+1]) + if !ok { + return uuid, errors.New("invalid UUID format") + } + uuid[i] = v + } + return uuid, nil +} + +// MustParse is like Parse but panics if the string cannot be parsed. +// It simplifies safe initialization of global variables holding compiled UUIDs. +func MustParse(s string) UUID { + uuid, err := Parse(s) + if err != nil { + panic(`uuid: Parse(` + s + `): ` + err.Error()) + } + return uuid +} + +// FromBytes creates a new UUID from a byte slice. Returns an error if the slice +// does not have a length of 16. The bytes are copied from the slice. +func FromBytes(b []byte) (uuid UUID, err error) { + err = uuid.UnmarshalBinary(b) + return uuid, err +} + +// Must returns uuid if err is nil and panics otherwise. +func Must(uuid UUID, err error) UUID { + if err != nil { + panic(err) + } + return uuid +} + +// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// , or "" if uuid is invalid. +func (uuid UUID) String() string { + var buf [36]byte + encodeHex(buf[:], uuid) + return string(buf[:]) +} + +// URN returns the RFC 2141 URN form of uuid, +// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. +func (uuid UUID) URN() string { + var buf [36 + 9]byte + copy(buf[:], "urn:uuid:") + encodeHex(buf[9:], uuid) + return string(buf[:]) +} + +func encodeHex(dst []byte, uuid UUID) { + hex.Encode(dst, uuid[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], uuid[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], uuid[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], uuid[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], uuid[10:]) +} + +// Variant returns the variant encoded in uuid. +func (uuid UUID) Variant() Variant { + switch { + case (uuid[8] & 0xc0) == 0x80: + return RFC4122 + case (uuid[8] & 0xe0) == 0xc0: + return Microsoft + case (uuid[8] & 0xe0) == 0xe0: + return Future + default: + return Reserved + } +} + +// Version returns the version of uuid. +func (uuid UUID) Version() Version { + return Version(uuid[6] >> 4) +} + +func (v Version) String() string { + if v > 15 { + return fmt.Sprintf("BAD_VERSION_%d", v) + } + return fmt.Sprintf("VERSION_%d", v) +} + +func (v Variant) String() string { + switch v { + case RFC4122: + return "RFC4122" + case Reserved: + return "Reserved" + case Microsoft: + return "Microsoft" + case Future: + return "Future" + case Invalid: + return "Invalid" + } + return fmt.Sprintf("BadVariant%d", int(v)) +} + +// SetRand sets the random number generator to r, which implements io.Reader. +// If r.Read returns an error when the package requests random data then +// a panic will be issued. +// +// Calling SetRand with nil sets the random number generator to the default +// generator. +func SetRand(r io.Reader) { + if r == nil { + rander = rand.Reader + return + } + rander = r +} + +// EnableRandPool enables internal randomness pool used for Random +// (Version 4) UUID generation. The pool contains random bytes read from +// the random number generator on demand in batches. Enabling the pool +// may improve the UUID generation throughput significantly. +// +// Since the pool is stored on the Go heap, this feature may be a bad fit +// for security sensitive applications. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func EnableRandPool() { + poolEnabled = true +} + +// DisableRandPool disables the randomness pool if it was previously +// enabled with EnableRandPool. +// +// Both EnableRandPool and DisableRandPool are not thread-safe and should +// only be called when there is no possibility that New or any other +// UUID Version 4 generation function will be called concurrently. +func DisableRandPool() { + poolEnabled = false + defer poolMu.Unlock() + poolMu.Lock() + poolPos = randPoolSize +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/version1.go b/project/10-posts/src/vendor/github.com/google/uuid/version1.go new file mode 100644 index 0000000..4631096 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/version1.go @@ -0,0 +1,44 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" +) + +// NewUUID returns a Version 1 UUID based on the current NodeID and clock +// sequence, and the current time. If the NodeID has not been set by SetNodeID +// or SetNodeInterface then it will be set automatically. If the NodeID cannot +// be set NewUUID returns nil. If clock sequence has not been set by +// SetClockSequence then it will be set automatically. If GetTime fails to +// return the current NewUUID returns nil and an error. +// +// In most cases, New should be used. +func NewUUID() (UUID, error) { + var uuid UUID + now, seq, err := GetTime() + if err != nil { + return uuid, err + } + + timeLow := uint32(now & 0xffffffff) + timeMid := uint16((now >> 32) & 0xffff) + timeHi := uint16((now >> 48) & 0x0fff) + timeHi |= 0x1000 // Version 1 + + binary.BigEndian.PutUint32(uuid[0:], timeLow) + binary.BigEndian.PutUint16(uuid[4:], timeMid) + binary.BigEndian.PutUint16(uuid[6:], timeHi) + binary.BigEndian.PutUint16(uuid[8:], seq) + + nodeMu.Lock() + if nodeID == zeroID { + setNodeInterface("") + } + copy(uuid[10:], nodeID[:]) + nodeMu.Unlock() + + return uuid, nil +} diff --git a/project/10-posts/src/vendor/github.com/google/uuid/version4.go b/project/10-posts/src/vendor/github.com/google/uuid/version4.go new file mode 100644 index 0000000..7697802 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/google/uuid/version4.go @@ -0,0 +1,76 @@ +// Copyright 2016 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import "io" + +// New creates a new random UUID or panics. New is equivalent to +// the expression +// +// uuid.Must(uuid.NewRandom()) +func New() UUID { + return Must(NewRandom()) +} + +// NewString creates a new random UUID and returns it as a string or panics. +// NewString is equivalent to the expression +// +// uuid.New().String() +func NewString() string { + return Must(NewRandom()).String() +} + +// NewRandom returns a Random (Version 4) UUID. +// +// The strength of the UUIDs is based on the strength of the crypto/rand +// package. +// +// Uses the randomness pool if it was enabled with EnableRandPool. +// +// A note about uniqueness derived from the UUID Wikipedia entry: +// +// Randomly generated UUIDs have 122 random bits. One's annual risk of being +// hit by a meteorite is estimated to be one chance in 17 billion, that +// means the probability is about 0.00000000006 (6 × 10−11), +// equivalent to the odds of creating a few tens of trillions of UUIDs in a +// year and having one duplicate. +func NewRandom() (UUID, error) { + if !poolEnabled { + return NewRandomFromReader(rander) + } + return newRandomFromPool() +} + +// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. +func NewRandomFromReader(r io.Reader) (UUID, error) { + var uuid UUID + _, err := io.ReadFull(r, uuid[:]) + if err != nil { + return Nil, err + } + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} + +func newRandomFromPool() (UUID, error) { + var uuid UUID + poolMu.Lock() + if poolPos == randPoolSize { + _, err := io.ReadFull(rander, pool[:]) + if err != nil { + poolMu.Unlock() + return Nil, err + } + poolPos = 0 + } + copy(uuid[:], pool[poolPos:(poolPos+16)]) + poolPos += 16 + poolMu.Unlock() + + uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 + uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 + return uuid, nil +} diff --git a/project/10-posts/src/vendor/github.com/joho/godotenv/.gitignore b/project/10-posts/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/10-posts/src/vendor/github.com/joho/godotenv/LICENCE b/project/10-posts/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/10-posts/src/vendor/github.com/joho/godotenv/README.md b/project/10-posts/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/10-posts/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/10-posts/src/vendor/github.com/joho/godotenv/godotenv.go b/project/10-posts/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/10-posts/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/10-posts/src/vendor/github.com/joho/godotenv/parser.go b/project/10-posts/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/10-posts/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/.gitignore b/project/10-posts/src/vendor/github.com/lib/pq/.gitignore new file mode 100644 index 0000000..3243952 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/.gitignore @@ -0,0 +1,6 @@ +.db +*.test +*~ +*.swp +.idea +.vscode \ No newline at end of file diff --git a/project/10-posts/src/vendor/github.com/lib/pq/LICENSE.md b/project/10-posts/src/vendor/github.com/lib/pq/LICENSE.md new file mode 100644 index 0000000..5773904 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/LICENSE.md @@ -0,0 +1,8 @@ +Copyright (c) 2011-2013, 'pq' Contributors +Portions Copyright (C) 2011 Blake Mizerany + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/10-posts/src/vendor/github.com/lib/pq/README.md b/project/10-posts/src/vendor/github.com/lib/pq/README.md new file mode 100644 index 0000000..126ee5d --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/README.md @@ -0,0 +1,36 @@ +# pq - A pure Go postgres driver for Go's database/sql package + +[![GoDoc](https://godoc.org/github.com/lib/pq?status.svg)](https://pkg.go.dev/github.com/lib/pq?tab=doc) + +## Install + + go get github.com/lib/pq + +## Features + +* SSL +* Handles bad connections for `database/sql` +* Scan `time.Time` correctly (i.e. `timestamp[tz]`, `time[tz]`, `date`) +* Scan binary blobs correctly (i.e. `bytea`) +* Package for `hstore` support +* COPY FROM support +* pq.ParseURL for converting urls to connection strings for sql.Open. +* Many libpq compatible environment variables +* Unix socket support +* Notifications: `LISTEN`/`NOTIFY` +* pgpass support +* GSS (Kerberos) auth + +## Tests + +`go test` is used for testing. See [TESTS.md](TESTS.md) for more details. + +## Status + +This package is currently in maintenance mode, which means: +1. It generally does not accept new features. +2. It does accept bug fixes and version compatability changes provided by the community. +3. Maintainers usually do not resolve reported issues. +4. Community members are encouraged to help each other with reported issues. + +For users that require new features or reliable resolution of reported bugs, we recommend using [pgx](https://github.com/jackc/pgx) which is under active development. diff --git a/project/10-posts/src/vendor/github.com/lib/pq/TESTS.md b/project/10-posts/src/vendor/github.com/lib/pq/TESTS.md new file mode 100644 index 0000000..f050211 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/TESTS.md @@ -0,0 +1,33 @@ +# Tests + +## Running Tests + +`go test` is used for testing. A running PostgreSQL +server is required, with the ability to log in. The +database to connect to test with is "pqgotest," on +"localhost" but these can be overridden using [environment +variables](https://www.postgresql.org/docs/9.3/static/libpq-envars.html). + +Example: + + PGHOST=/run/postgresql go test + +## Benchmarks + +A benchmark suite can be run as part of the tests: + + go test -bench . + +## Example setup (Docker) + +Run a postgres container: + +``` +docker run --expose 5432:5432 postgres +``` + +Run tests: + +``` +PGHOST=localhost PGPORT=5432 PGUSER=postgres PGSSLMODE=disable PGDATABASE=postgres go test +``` diff --git a/project/10-posts/src/vendor/github.com/lib/pq/array.go b/project/10-posts/src/vendor/github.com/lib/pq/array.go new file mode 100644 index 0000000..39c8f7e --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/array.go @@ -0,0 +1,895 @@ +package pq + +import ( + "bytes" + "database/sql" + "database/sql/driver" + "encoding/hex" + "fmt" + "reflect" + "strconv" + "strings" +) + +var typeByteSlice = reflect.TypeOf([]byte{}) +var typeDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem() +var typeSQLScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem() + +// Array returns the optimal driver.Valuer and sql.Scanner for an array or +// slice of any dimension. +// +// For example: +// db.Query(`SELECT * FROM t WHERE id = ANY($1)`, pq.Array([]int{235, 401})) +// +// var x []sql.NullInt64 +// db.QueryRow(`SELECT ARRAY[235, 401]`).Scan(pq.Array(&x)) +// +// Scanning multi-dimensional arrays is not supported. Arrays where the lower +// bound is not one (such as `[0:0]={1}') are not supported. +func Array(a interface{}) interface { + driver.Valuer + sql.Scanner +} { + switch a := a.(type) { + case []bool: + return (*BoolArray)(&a) + case []float64: + return (*Float64Array)(&a) + case []float32: + return (*Float32Array)(&a) + case []int64: + return (*Int64Array)(&a) + case []int32: + return (*Int32Array)(&a) + case []string: + return (*StringArray)(&a) + case [][]byte: + return (*ByteaArray)(&a) + + case *[]bool: + return (*BoolArray)(a) + case *[]float64: + return (*Float64Array)(a) + case *[]float32: + return (*Float32Array)(a) + case *[]int64: + return (*Int64Array)(a) + case *[]int32: + return (*Int32Array)(a) + case *[]string: + return (*StringArray)(a) + case *[][]byte: + return (*ByteaArray)(a) + } + + return GenericArray{a} +} + +// ArrayDelimiter may be optionally implemented by driver.Valuer or sql.Scanner +// to override the array delimiter used by GenericArray. +type ArrayDelimiter interface { + // ArrayDelimiter returns the delimiter character(s) for this element's type. + ArrayDelimiter() string +} + +// BoolArray represents a one-dimensional array of the PostgreSQL boolean type. +type BoolArray []bool + +// Scan implements the sql.Scanner interface. +func (a *BoolArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to BoolArray", src) +} + +func (a *BoolArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "BoolArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(BoolArray, len(elems)) + for i, v := range elems { + if len(v) != 1 { + return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v) + } + switch v[0] { + case 't': + b[i] = true + case 'f': + b[i] = false + default: + return fmt.Errorf("pq: could not parse boolean array index %d: invalid boolean %q", i, v) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a BoolArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be exactly two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1+2*n) + + for i := 0; i < n; i++ { + b[2*i] = ',' + if a[i] { + b[1+2*i] = 't' + } else { + b[1+2*i] = 'f' + } + } + + b[0] = '{' + b[2*n] = '}' + + return string(b), nil + } + + return "{}", nil +} + +// ByteaArray represents a one-dimensional array of the PostgreSQL bytea type. +type ByteaArray [][]byte + +// Scan implements the sql.Scanner interface. +func (a *ByteaArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to ByteaArray", src) +} + +func (a *ByteaArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "ByteaArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(ByteaArray, len(elems)) + for i, v := range elems { + b[i], err = parseBytea(v) + if err != nil { + return fmt.Errorf("could not parse bytea array index %d: %s", i, err.Error()) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. It uses the "hex" format which +// is only supported on PostgreSQL 9.0 or newer. +func (a ByteaArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, 2*N bytes of quotes, + // 3*N bytes of hex formatting, and N-1 bytes of delimiters. + size := 1 + 6*n + for _, x := range a { + size += hex.EncodedLen(len(x)) + } + + b := make([]byte, size) + + for i, s := 0, b; i < n; i++ { + o := copy(s, `,"\\x`) + o += hex.Encode(s[o:], a[i]) + s[o] = '"' + s = s[o+1:] + } + + b[0] = '{' + b[size-1] = '}' + + return string(b), nil + } + + return "{}", nil +} + +// Float64Array represents a one-dimensional array of the PostgreSQL double +// precision type. +type Float64Array []float64 + +// Scan implements the sql.Scanner interface. +func (a *Float64Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Float64Array", src) +} + +func (a *Float64Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Float64Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Float64Array, len(elems)) + for i, v := range elems { + if b[i], err = strconv.ParseFloat(string(v), 64); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Float64Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendFloat(b, a[0], 'f', -1, 64) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendFloat(b, a[i], 'f', -1, 64) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// Float32Array represents a one-dimensional array of the PostgreSQL double +// precision type. +type Float32Array []float32 + +// Scan implements the sql.Scanner interface. +func (a *Float32Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Float32Array", src) +} + +func (a *Float32Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Float32Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Float32Array, len(elems)) + for i, v := range elems { + var x float64 + if x, err = strconv.ParseFloat(string(v), 32); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + b[i] = float32(x) + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Float32Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendFloat(b, float64(a[0]), 'f', -1, 32) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendFloat(b, float64(a[i]), 'f', -1, 32) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// GenericArray implements the driver.Valuer and sql.Scanner interfaces for +// an array or slice of any dimension. +type GenericArray struct{ A interface{} } + +func (GenericArray) evaluateDestination(rt reflect.Type) (reflect.Type, func([]byte, reflect.Value) error, string) { + var assign func([]byte, reflect.Value) error + var del = "," + + // TODO calculate the assign function for other types + // TODO repeat this section on the element type of arrays or slices (multidimensional) + { + if reflect.PtrTo(rt).Implements(typeSQLScanner) { + // dest is always addressable because it is an element of a slice. + assign = func(src []byte, dest reflect.Value) (err error) { + ss := dest.Addr().Interface().(sql.Scanner) + if src == nil { + err = ss.Scan(nil) + } else { + err = ss.Scan(src) + } + return + } + goto FoundType + } + + assign = func([]byte, reflect.Value) error { + return fmt.Errorf("pq: scanning to %s is not implemented; only sql.Scanner", rt) + } + } + +FoundType: + + if ad, ok := reflect.Zero(rt).Interface().(ArrayDelimiter); ok { + del = ad.ArrayDelimiter() + } + + return rt, assign, del +} + +// Scan implements the sql.Scanner interface. +func (a GenericArray) Scan(src interface{}) error { + dpv := reflect.ValueOf(a.A) + switch { + case dpv.Kind() != reflect.Ptr: + return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A) + case dpv.IsNil(): + return fmt.Errorf("pq: destination %T is nil", a.A) + } + + dv := dpv.Elem() + switch dv.Kind() { + case reflect.Slice: + case reflect.Array: + default: + return fmt.Errorf("pq: destination %T is not a pointer to array or slice", a.A) + } + + switch src := src.(type) { + case []byte: + return a.scanBytes(src, dv) + case string: + return a.scanBytes([]byte(src), dv) + case nil: + if dv.Kind() == reflect.Slice { + dv.Set(reflect.Zero(dv.Type())) + return nil + } + } + + return fmt.Errorf("pq: cannot convert %T to %s", src, dv.Type()) +} + +func (a GenericArray) scanBytes(src []byte, dv reflect.Value) error { + dtype, assign, del := a.evaluateDestination(dv.Type().Elem()) + dims, elems, err := parseArray(src, []byte(del)) + if err != nil { + return err + } + + // TODO allow multidimensional + + if len(dims) > 1 { + return fmt.Errorf("pq: scanning from multidimensional ARRAY%s is not implemented", + strings.Replace(fmt.Sprint(dims), " ", "][", -1)) + } + + // Treat a zero-dimensional array like an array with a single dimension of zero. + if len(dims) == 0 { + dims = append(dims, 0) + } + + for i, rt := 0, dv.Type(); i < len(dims); i, rt = i+1, rt.Elem() { + switch rt.Kind() { + case reflect.Slice: + case reflect.Array: + if rt.Len() != dims[i] { + return fmt.Errorf("pq: cannot convert ARRAY%s to %s", + strings.Replace(fmt.Sprint(dims), " ", "][", -1), dv.Type()) + } + default: + // TODO handle multidimensional + } + } + + values := reflect.MakeSlice(reflect.SliceOf(dtype), len(elems), len(elems)) + for i, e := range elems { + if err := assign(e, values.Index(i)); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + + // TODO handle multidimensional + + switch dv.Kind() { + case reflect.Slice: + dv.Set(values.Slice(0, dims[0])) + case reflect.Array: + for i := 0; i < dims[0]; i++ { + dv.Index(i).Set(values.Index(i)) + } + } + + return nil +} + +// Value implements the driver.Valuer interface. +func (a GenericArray) Value() (driver.Value, error) { + if a.A == nil { + return nil, nil + } + + rv := reflect.ValueOf(a.A) + + switch rv.Kind() { + case reflect.Slice: + if rv.IsNil() { + return nil, nil + } + case reflect.Array: + default: + return nil, fmt.Errorf("pq: Unable to convert %T to array", a.A) + } + + if n := rv.Len(); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 0, 1+2*n) + + b, _, err := appendArray(b, rv, n) + return string(b), err + } + + return "{}", nil +} + +// Int64Array represents a one-dimensional array of the PostgreSQL integer types. +type Int64Array []int64 + +// Scan implements the sql.Scanner interface. +func (a *Int64Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Int64Array", src) +} + +func (a *Int64Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Int64Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Int64Array, len(elems)) + for i, v := range elems { + if b[i], err = strconv.ParseInt(string(v), 10, 64); err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Int64Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendInt(b, a[0], 10) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendInt(b, a[i], 10) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// Int32Array represents a one-dimensional array of the PostgreSQL integer types. +type Int32Array []int32 + +// Scan implements the sql.Scanner interface. +func (a *Int32Array) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to Int32Array", src) +} + +func (a *Int32Array) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "Int32Array") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(Int32Array, len(elems)) + for i, v := range elems { + x, err := strconv.ParseInt(string(v), 10, 32) + if err != nil { + return fmt.Errorf("pq: parsing array element index %d: %v", i, err) + } + b[i] = int32(x) + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a Int32Array) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, N bytes of values, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+2*n) + b[0] = '{' + + b = strconv.AppendInt(b, int64(a[0]), 10) + for i := 1; i < n; i++ { + b = append(b, ',') + b = strconv.AppendInt(b, int64(a[i]), 10) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// StringArray represents a one-dimensional array of the PostgreSQL character types. +type StringArray []string + +// Scan implements the sql.Scanner interface. +func (a *StringArray) Scan(src interface{}) error { + switch src := src.(type) { + case []byte: + return a.scanBytes(src) + case string: + return a.scanBytes([]byte(src)) + case nil: + *a = nil + return nil + } + + return fmt.Errorf("pq: cannot convert %T to StringArray", src) +} + +func (a *StringArray) scanBytes(src []byte) error { + elems, err := scanLinearArray(src, []byte{','}, "StringArray") + if err != nil { + return err + } + if *a != nil && len(elems) == 0 { + *a = (*a)[:0] + } else { + b := make(StringArray, len(elems)) + for i, v := range elems { + if b[i] = string(v); v == nil { + return fmt.Errorf("pq: parsing array element index %d: cannot convert nil to string", i) + } + } + *a = b + } + return nil +} + +// Value implements the driver.Valuer interface. +func (a StringArray) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + + if n := len(a); n > 0 { + // There will be at least two curly brackets, 2*N bytes of quotes, + // and N-1 bytes of delimiters. + b := make([]byte, 1, 1+3*n) + b[0] = '{' + + b = appendArrayQuotedBytes(b, []byte(a[0])) + for i := 1; i < n; i++ { + b = append(b, ',') + b = appendArrayQuotedBytes(b, []byte(a[i])) + } + + return string(append(b, '}')), nil + } + + return "{}", nil +} + +// appendArray appends rv to the buffer, returning the extended buffer and +// the delimiter used between elements. +// +// It panics when n <= 0 or rv's Kind is not reflect.Array nor reflect.Slice. +func appendArray(b []byte, rv reflect.Value, n int) ([]byte, string, error) { + var del string + var err error + + b = append(b, '{') + + if b, del, err = appendArrayElement(b, rv.Index(0)); err != nil { + return b, del, err + } + + for i := 1; i < n; i++ { + b = append(b, del...) + if b, del, err = appendArrayElement(b, rv.Index(i)); err != nil { + return b, del, err + } + } + + return append(b, '}'), del, nil +} + +// appendArrayElement appends rv to the buffer, returning the extended buffer +// and the delimiter to use before the next element. +// +// When rv's Kind is neither reflect.Array nor reflect.Slice, it is converted +// using driver.DefaultParameterConverter and the resulting []byte or string +// is double-quoted. +// +// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO +func appendArrayElement(b []byte, rv reflect.Value) ([]byte, string, error) { + if k := rv.Kind(); k == reflect.Array || k == reflect.Slice { + if t := rv.Type(); t != typeByteSlice && !t.Implements(typeDriverValuer) { + if n := rv.Len(); n > 0 { + return appendArray(b, rv, n) + } + + return b, "", nil + } + } + + var del = "," + var err error + var iv interface{} = rv.Interface() + + if ad, ok := iv.(ArrayDelimiter); ok { + del = ad.ArrayDelimiter() + } + + if iv, err = driver.DefaultParameterConverter.ConvertValue(iv); err != nil { + return b, del, err + } + + switch v := iv.(type) { + case nil: + return append(b, "NULL"...), del, nil + case []byte: + return appendArrayQuotedBytes(b, v), del, nil + case string: + return appendArrayQuotedBytes(b, []byte(v)), del, nil + } + + b, err = appendValue(b, iv) + return b, del, err +} + +func appendArrayQuotedBytes(b, v []byte) []byte { + b = append(b, '"') + for { + i := bytes.IndexAny(v, `"\`) + if i < 0 { + b = append(b, v...) + break + } + if i > 0 { + b = append(b, v[:i]...) + } + b = append(b, '\\', v[i]) + v = v[i+1:] + } + return append(b, '"') +} + +func appendValue(b []byte, v driver.Value) ([]byte, error) { + return append(b, encode(nil, v, 0)...), nil +} + +// parseArray extracts the dimensions and elements of an array represented in +// text format. Only representations emitted by the backend are supported. +// Notably, whitespace around brackets and delimiters is significant, and NULL +// is case-sensitive. +// +// See http://www.postgresql.org/docs/current/static/arrays.html#ARRAYS-IO +func parseArray(src, del []byte) (dims []int, elems [][]byte, err error) { + var depth, i int + + if len(src) < 1 || src[0] != '{' { + return nil, nil, fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '{', 0) + } + +Open: + for i < len(src) { + switch src[i] { + case '{': + depth++ + i++ + case '}': + elems = make([][]byte, 0) + goto Close + default: + break Open + } + } + dims = make([]int, i) + +Element: + for i < len(src) { + switch src[i] { + case '{': + if depth == len(dims) { + break Element + } + depth++ + dims[depth-1] = 0 + i++ + case '"': + var elem = []byte{} + var escape bool + for i++; i < len(src); i++ { + if escape { + elem = append(elem, src[i]) + escape = false + } else { + switch src[i] { + default: + elem = append(elem, src[i]) + case '\\': + escape = true + case '"': + elems = append(elems, elem) + i++ + break Element + } + } + } + default: + for start := i; i < len(src); i++ { + if bytes.HasPrefix(src[i:], del) || src[i] == '}' { + elem := src[start:i] + if len(elem) == 0 { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + if bytes.Equal(elem, []byte("NULL")) { + elem = nil + } + elems = append(elems, elem) + break Element + } + } + } + } + + for i < len(src) { + if bytes.HasPrefix(src[i:], del) && depth > 0 { + dims[depth-1]++ + i += len(del) + goto Element + } else if src[i] == '}' && depth > 0 { + dims[depth-1]++ + depth-- + i++ + } else { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + } + +Close: + for i < len(src) { + if src[i] == '}' && depth > 0 { + depth-- + i++ + } else { + return nil, nil, fmt.Errorf("pq: unable to parse array; unexpected %q at offset %d", src[i], i) + } + } + if depth > 0 { + err = fmt.Errorf("pq: unable to parse array; expected %q at offset %d", '}', i) + } + if err == nil { + for _, d := range dims { + if (len(elems) % d) != 0 { + err = fmt.Errorf("pq: multidimensional arrays must have elements with matching dimensions") + } + } + } + return +} + +func scanLinearArray(src, del []byte, typ string) (elems [][]byte, err error) { + dims, elems, err := parseArray(src, del) + if err != nil { + return nil, err + } + if len(dims) > 1 { + return nil, fmt.Errorf("pq: cannot convert ARRAY%s to %s", strings.Replace(fmt.Sprint(dims), " ", "][", -1), typ) + } + return elems, err +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/buf.go b/project/10-posts/src/vendor/github.com/lib/pq/buf.go new file mode 100644 index 0000000..4b0a0a8 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/buf.go @@ -0,0 +1,91 @@ +package pq + +import ( + "bytes" + "encoding/binary" + + "github.com/lib/pq/oid" +) + +type readBuf []byte + +func (b *readBuf) int32() (n int) { + n = int(int32(binary.BigEndian.Uint32(*b))) + *b = (*b)[4:] + return +} + +func (b *readBuf) oid() (n oid.Oid) { + n = oid.Oid(binary.BigEndian.Uint32(*b)) + *b = (*b)[4:] + return +} + +// N.B: this is actually an unsigned 16-bit integer, unlike int32 +func (b *readBuf) int16() (n int) { + n = int(binary.BigEndian.Uint16(*b)) + *b = (*b)[2:] + return +} + +func (b *readBuf) string() string { + i := bytes.IndexByte(*b, 0) + if i < 0 { + errorf("invalid message format; expected string terminator") + } + s := (*b)[:i] + *b = (*b)[i+1:] + return string(s) +} + +func (b *readBuf) next(n int) (v []byte) { + v = (*b)[:n] + *b = (*b)[n:] + return +} + +func (b *readBuf) byte() byte { + return b.next(1)[0] +} + +type writeBuf struct { + buf []byte + pos int +} + +func (b *writeBuf) int32(n int) { + x := make([]byte, 4) + binary.BigEndian.PutUint32(x, uint32(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) int16(n int) { + x := make([]byte, 2) + binary.BigEndian.PutUint16(x, uint16(n)) + b.buf = append(b.buf, x...) +} + +func (b *writeBuf) string(s string) { + b.buf = append(append(b.buf, s...), '\000') +} + +func (b *writeBuf) byte(c byte) { + b.buf = append(b.buf, c) +} + +func (b *writeBuf) bytes(v []byte) { + b.buf = append(b.buf, v...) +} + +func (b *writeBuf) wrap() []byte { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + return b.buf +} + +func (b *writeBuf) next(c byte) { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + b.pos = len(b.buf) + 1 + b.buf = append(b.buf, c, 0, 0, 0, 0) +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/conn.go b/project/10-posts/src/vendor/github.com/lib/pq/conn.go new file mode 100644 index 0000000..e70b386 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/conn.go @@ -0,0 +1,2064 @@ +package pq + +import ( + "bufio" + "context" + "crypto/md5" + "crypto/sha256" + "database/sql" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "os" + "os/user" + "path" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + "unicode" + + "github.com/lib/pq/oid" + "github.com/lib/pq/scram" +) + +// Common error types +var ( + ErrNotSupported = errors.New("pq: Unsupported command") + ErrInFailedTransaction = errors.New("pq: Could not complete operation in a failed transaction") + ErrSSLNotSupported = errors.New("pq: SSL is not enabled on the server") + ErrSSLKeyUnknownOwnership = errors.New("pq: Could not get owner information for private key, may not be properly protected") + ErrSSLKeyHasWorldPermissions = errors.New("pq: Private key has world access. Permissions should be u=rw,g=r (0640) if owned by root, or u=rw (0600), or less") + + ErrCouldNotDetectUsername = errors.New("pq: Could not detect default username. Please provide one explicitly") + + errUnexpectedReady = errors.New("unexpected ReadyForQuery") + errNoRowsAffected = errors.New("no RowsAffected available after the empty statement") + errNoLastInsertID = errors.New("no LastInsertId available after the empty statement") +) + +// Compile time validation that our types implement the expected interfaces +var ( + _ driver.Driver = Driver{} +) + +// Driver is the Postgres database driver. +type Driver struct{} + +// Open opens a new connection to the database. name is a connection string. +// Most users should only use it through database/sql package from the standard +// library. +func (d Driver) Open(name string) (driver.Conn, error) { + return Open(name) +} + +func init() { + sql.Register("postgres", &Driver{}) +} + +type parameterStatus struct { + // server version in the same format as server_version_num, or 0 if + // unavailable + serverVersion int + + // the current location based on the TimeZone value of the session, if + // available + currentLocation *time.Location +} + +type transactionStatus byte + +const ( + txnStatusIdle transactionStatus = 'I' + txnStatusIdleInTransaction transactionStatus = 'T' + txnStatusInFailedTransaction transactionStatus = 'E' +) + +func (s transactionStatus) String() string { + switch s { + case txnStatusIdle: + return "idle" + case txnStatusIdleInTransaction: + return "idle in transaction" + case txnStatusInFailedTransaction: + return "in a failed transaction" + default: + errorf("unknown transactionStatus %d", s) + } + + panic("not reached") +} + +// Dialer is the dialer interface. It can be used to obtain more control over +// how pq creates network connections. +type Dialer interface { + Dial(network, address string) (net.Conn, error) + DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) +} + +// DialerContext is the context-aware dialer interface. +type DialerContext interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +type defaultDialer struct { + d net.Dialer +} + +func (d defaultDialer) Dial(network, address string) (net.Conn, error) { + return d.d.Dial(network, address) +} +func (d defaultDialer) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return d.DialContext(ctx, network, address) +} +func (d defaultDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return d.d.DialContext(ctx, network, address) +} + +type conn struct { + c net.Conn + buf *bufio.Reader + namei int + scratch [512]byte + txnStatus transactionStatus + txnFinish func() + + // Save connection arguments to use during CancelRequest. + dialer Dialer + opts values + + // Cancellation key data for use with CancelRequest messages. + processID int + secretKey int + + parameterStatus parameterStatus + + saveMessageType byte + saveMessageBuffer []byte + + // If an error is set, this connection is bad and all public-facing + // functions should return the appropriate error by calling get() + // (ErrBadConn) or getForNext(). + err syncErr + + // If set, this connection should never use the binary format when + // receiving query results from prepared statements. Only provided for + // debugging. + disablePreparedBinaryResult bool + + // Whether to always send []byte parameters over as binary. Enables single + // round-trip mode for non-prepared Query calls. + binaryParameters bool + + // If true this connection is in the middle of a COPY + inCopy bool + + // If not nil, notices will be synchronously sent here + noticeHandler func(*Error) + + // If not nil, notifications will be synchronously sent here + notificationHandler func(*Notification) + + // GSSAPI context + gss GSS +} + +type syncErr struct { + err error + sync.Mutex +} + +// Return ErrBadConn if connection is bad. +func (e *syncErr) get() error { + e.Lock() + defer e.Unlock() + if e.err != nil { + return driver.ErrBadConn + } + return nil +} + +// Return the error set on the connection. Currently only used by rows.Next. +func (e *syncErr) getForNext() error { + e.Lock() + defer e.Unlock() + return e.err +} + +// Set error, only if it isn't set yet. +func (e *syncErr) set(err error) { + if err == nil { + panic("attempt to set nil err") + } + e.Lock() + defer e.Unlock() + if e.err == nil { + e.err = err + } +} + +// Handle driver-side settings in parsed connection string. +func (cn *conn) handleDriverSettings(o values) (err error) { + boolSetting := func(key string, val *bool) error { + if value, ok := o[key]; ok { + if value == "yes" { + *val = true + } else if value == "no" { + *val = false + } else { + return fmt.Errorf("unrecognized value %q for %s", value, key) + } + } + return nil + } + + err = boolSetting("disable_prepared_binary_result", &cn.disablePreparedBinaryResult) + if err != nil { + return err + } + return boolSetting("binary_parameters", &cn.binaryParameters) +} + +func (cn *conn) handlePgpass(o values) { + // if a password was supplied, do not process .pgpass + if _, ok := o["password"]; ok { + return + } + filename := os.Getenv("PGPASSFILE") + if filename == "" { + // XXX this code doesn't work on Windows where the default filename is + // XXX %APPDATA%\postgresql\pgpass.conf + // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 + userHome := os.Getenv("HOME") + if userHome == "" { + user, err := user.Current() + if err != nil { + return + } + userHome = user.HomeDir + } + filename = filepath.Join(userHome, ".pgpass") + } + fileinfo, err := os.Stat(filename) + if err != nil { + return + } + mode := fileinfo.Mode() + if mode&(0x77) != 0 { + // XXX should warn about incorrect .pgpass permissions as psql does + return + } + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + scanner := bufio.NewScanner(io.Reader(file)) + hostname := o["host"] + ntw, _ := network(o) + port := o["port"] + db := o["dbname"] + username := o["user"] + // From: https://github.com/tg/pgpass/blob/master/reader.go + getFields := func(s string) []string { + fs := make([]string, 0, 5) + f := make([]rune, 0, len(s)) + + var esc bool + for _, c := range s { + switch { + case esc: + f = append(f, c) + esc = false + case c == '\\': + esc = true + case c == ':': + fs = append(fs, string(f)) + f = f[:0] + default: + f = append(f, c) + } + } + return append(fs, string(f)) + } + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || line[0] == '#' { + continue + } + split := getFields(line) + if len(split) != 5 { + continue + } + if (split[0] == "*" || split[0] == hostname || (split[0] == "localhost" && (hostname == "" || ntw == "unix"))) && (split[1] == "*" || split[1] == port) && (split[2] == "*" || split[2] == db) && (split[3] == "*" || split[3] == username) { + o["password"] = split[4] + return + } + } +} + +func (cn *conn) writeBuf(b byte) *writeBuf { + cn.scratch[0] = b + return &writeBuf{ + buf: cn.scratch[:5], + pos: 1, + } +} + +// Open opens a new connection to the database. dsn is a connection string. +// Most users should only use it through database/sql package from the standard +// library. +func Open(dsn string) (_ driver.Conn, err error) { + return DialOpen(defaultDialer{}, dsn) +} + +// DialOpen opens a new connection to the database using a dialer. +func DialOpen(d Dialer, dsn string) (_ driver.Conn, err error) { + c, err := NewConnector(dsn) + if err != nil { + return nil, err + } + c.Dialer(d) + return c.open(context.Background()) +} + +func (c *Connector) open(ctx context.Context) (cn *conn, err error) { + // Handle any panics during connection initialization. Note that we + // specifically do *not* want to use errRecover(), as that would turn any + // connection errors into ErrBadConns, hiding the real error message from + // the user. + defer errRecoverNoErrBadConn(&err) + + // Create a new values map (copy). This makes it so maps in different + // connections do not reference the same underlying data structure, so it + // is safe for multiple connections to concurrently write to their opts. + o := make(values) + for k, v := range c.opts { + o[k] = v + } + + cn = &conn{ + opts: o, + dialer: c.dialer, + } + err = cn.handleDriverSettings(o) + if err != nil { + return nil, err + } + cn.handlePgpass(o) + + cn.c, err = dial(ctx, c.dialer, o) + if err != nil { + return nil, err + } + + err = cn.ssl(o) + if err != nil { + if cn.c != nil { + cn.c.Close() + } + return nil, err + } + + // cn.startup panics on error. Make sure we don't leak cn.c. + panicking := true + defer func() { + if panicking { + cn.c.Close() + } + }() + + cn.buf = bufio.NewReader(cn.c) + cn.startup(o) + + // reset the deadline, in case one was set (see dial) + if timeout, ok := o["connect_timeout"]; ok && timeout != "0" { + err = cn.c.SetDeadline(time.Time{}) + } + panicking = false + return cn, err +} + +func dial(ctx context.Context, d Dialer, o values) (net.Conn, error) { + network, address := network(o) + + // Zero or not specified means wait indefinitely. + if timeout, ok := o["connect_timeout"]; ok && timeout != "0" { + seconds, err := strconv.ParseInt(timeout, 10, 0) + if err != nil { + return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) + } + duration := time.Duration(seconds) * time.Second + + // connect_timeout should apply to the entire connection establishment + // procedure, so we both use a timeout for the TCP connection + // establishment and set a deadline for doing the initial handshake. + // The deadline is then reset after startup() is done. + deadline := time.Now().Add(duration) + var conn net.Conn + if dctx, ok := d.(DialerContext); ok { + ctx, cancel := context.WithTimeout(ctx, duration) + defer cancel() + conn, err = dctx.DialContext(ctx, network, address) + } else { + conn, err = d.DialTimeout(network, address, duration) + } + if err != nil { + return nil, err + } + err = conn.SetDeadline(deadline) + return conn, err + } + if dctx, ok := d.(DialerContext); ok { + return dctx.DialContext(ctx, network, address) + } + return d.Dial(network, address) +} + +func network(o values) (string, string) { + host := o["host"] + + if strings.HasPrefix(host, "/") { + sockPath := path.Join(host, ".s.PGSQL."+o["port"]) + return "unix", sockPath + } + + return "tcp", net.JoinHostPort(host, o["port"]) +} + +type values map[string]string + +// scanner implements a tokenizer for libpq-style option strings. +type scanner struct { + s []rune + i int +} + +// newScanner returns a new scanner initialized with the option string s. +func newScanner(s string) *scanner { + return &scanner{[]rune(s), 0} +} + +// Next returns the next rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) Next() (rune, bool) { + if s.i >= len(s.s) { + return 0, false + } + r := s.s[s.i] + s.i++ + return r, true +} + +// SkipSpaces returns the next non-whitespace rune. +// It returns 0, false if the end of the text has been reached. +func (s *scanner) SkipSpaces() (rune, bool) { + r, ok := s.Next() + for unicode.IsSpace(r) && ok { + r, ok = s.Next() + } + return r, ok +} + +// parseOpts parses the options from name and adds them to the values. +// +// The parsing code is based on conninfo_parse from libpq's fe-connect.c +func parseOpts(name string, o values) error { + s := newScanner(name) + + for { + var ( + keyRunes, valRunes []rune + r rune + ok bool + ) + + if r, ok = s.SkipSpaces(); !ok { + break + } + + // Scan the key + for !unicode.IsSpace(r) && r != '=' { + keyRunes = append(keyRunes, r) + if r, ok = s.Next(); !ok { + break + } + } + + // Skip any whitespace if we're not at the = yet + if r != '=' { + r, ok = s.SkipSpaces() + } + + // The current character should be = + if r != '=' || !ok { + return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) + } + + // Skip any whitespace after the = + if r, ok = s.SkipSpaces(); !ok { + // If we reach the end here, the last value is just an empty string as per libpq. + o[string(keyRunes)] = "" + break + } + + if r != '\'' { + for !unicode.IsSpace(r) { + if r == '\\' { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`missing character after backslash`) + } + } + valRunes = append(valRunes, r) + + if r, ok = s.Next(); !ok { + break + } + } + } else { + quote: + for { + if r, ok = s.Next(); !ok { + return fmt.Errorf(`unterminated quoted string literal in connection string`) + } + switch r { + case '\'': + break quote + case '\\': + r, _ = s.Next() + fallthrough + default: + valRunes = append(valRunes, r) + } + } + } + + o[string(keyRunes)] = string(valRunes) + } + + return nil +} + +func (cn *conn) isInTransaction() bool { + return cn.txnStatus == txnStatusIdleInTransaction || + cn.txnStatus == txnStatusInFailedTransaction +} + +func (cn *conn) checkIsInTransaction(intxn bool) { + if cn.isInTransaction() != intxn { + cn.err.set(driver.ErrBadConn) + errorf("unexpected transaction status %v", cn.txnStatus) + } +} + +func (cn *conn) Begin() (_ driver.Tx, err error) { + return cn.begin("") +} + +func (cn *conn) begin(mode string) (_ driver.Tx, err error) { + if err := cn.err.get(); err != nil { + return nil, err + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(false) + _, commandTag, err := cn.simpleExec("BEGIN" + mode) + if err != nil { + return nil, err + } + if commandTag != "BEGIN" { + cn.err.set(driver.ErrBadConn) + return nil, fmt.Errorf("unexpected command tag %s", commandTag) + } + if cn.txnStatus != txnStatusIdleInTransaction { + cn.err.set(driver.ErrBadConn) + return nil, fmt.Errorf("unexpected transaction status %v", cn.txnStatus) + } + return cn, nil +} + +func (cn *conn) closeTxn() { + if finish := cn.txnFinish; finish != nil { + finish() + } +} + +func (cn *conn) Commit() (err error) { + defer cn.closeTxn() + if err := cn.err.get(); err != nil { + return err + } + defer cn.errRecover(&err) + + cn.checkIsInTransaction(true) + // We don't want the client to think that everything is okay if it tries + // to commit a failed transaction. However, no matter what we return, + // database/sql will release this connection back into the free connection + // pool so we have to abort the current transaction here. Note that you + // would get the same behaviour if you issued a COMMIT in a failed + // transaction, so it's also the least surprising thing to do here. + if cn.txnStatus == txnStatusInFailedTransaction { + if err := cn.rollback(); err != nil { + return err + } + return ErrInFailedTransaction + } + + _, commandTag, err := cn.simpleExec("COMMIT") + if err != nil { + if cn.isInTransaction() { + cn.err.set(driver.ErrBadConn) + } + return err + } + if commandTag != "COMMIT" { + cn.err.set(driver.ErrBadConn) + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) Rollback() (err error) { + defer cn.closeTxn() + if err := cn.err.get(); err != nil { + return err + } + defer cn.errRecover(&err) + return cn.rollback() +} + +func (cn *conn) rollback() (err error) { + cn.checkIsInTransaction(true) + _, commandTag, err := cn.simpleExec("ROLLBACK") + if err != nil { + if cn.isInTransaction() { + cn.err.set(driver.ErrBadConn) + } + return err + } + if commandTag != "ROLLBACK" { + return fmt.Errorf("unexpected command tag %s", commandTag) + } + cn.checkIsInTransaction(false) + return nil +} + +func (cn *conn) gname() string { + cn.namei++ + return strconv.FormatInt(int64(cn.namei), 10) +} + +func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err error) { + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C': + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + if res == nil && err == nil { + err = errUnexpectedReady + } + // done + return + case 'E': + err = parseError(r) + case 'I': + res = emptyRows + case 'T', 'D': + // ignore any results + default: + cn.err.set(driver.ErrBadConn) + errorf("unknown response for simple query: %q", t) + } + } +} + +func (cn *conn) simpleQuery(q string) (res *rows, err error) { + defer cn.errRecover(&err) + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'C', 'I': + // We allow queries which don't return any results through Query as + // well as Exec. We still have to give database/sql a rows object + // the user can close, though, to avoid connections from being + // leaked. A "rows" with done=true works fine for that purpose. + if err != nil { + cn.err.set(driver.ErrBadConn) + errorf("unexpected message %q in simple query execution", t) + } + if res == nil { + res = &rows{ + cn: cn, + } + } + // Set the result and tag to the last command complete if there wasn't a + // query already run. Although queries usually return from here and cede + // control to Next, a query with zero results does not. + if t == 'C' { + res.result, res.tag = cn.parseComplete(r.string()) + if res.colNames != nil { + return + } + } + res.done = true + case 'Z': + cn.processReadyForQuery(r) + // done + return + case 'E': + res = nil + err = parseError(r) + case 'D': + if res == nil { + cn.err.set(driver.ErrBadConn) + errorf("unexpected DataRow in simple query execution") + } + // the query didn't fail; kick off to Next + cn.saveMessage(t, r) + return + case 'T': + // res might be non-nil here if we received a previous + // CommandComplete, but that's fine; just overwrite it + res = &rows{cn: cn} + res.rowsHeader = parsePortalRowDescribe(r) + + // To work around a bug in QueryRow in Go 1.2 and earlier, wait + // until the first DataRow has been received. + default: + cn.err.set(driver.ErrBadConn) + errorf("unknown response for simple query: %q", t) + } + } +} + +type noRows struct{} + +var emptyRows noRows + +var _ driver.Result = noRows{} + +func (noRows) LastInsertId() (int64, error) { + return 0, errNoLastInsertID +} + +func (noRows) RowsAffected() (int64, error) { + return 0, errNoRowsAffected +} + +// Decides which column formats to use for a prepared statement. The input is +// an array of type oids, one element per result column. +func decideColumnFormats(colTyps []fieldDesc, forceText bool) (colFmts []format, colFmtData []byte) { + if len(colTyps) == 0 { + return nil, colFmtDataAllText + } + + colFmts = make([]format, len(colTyps)) + if forceText { + return colFmts, colFmtDataAllText + } + + allBinary := true + allText := true + for i, t := range colTyps { + switch t.OID { + // This is the list of types to use binary mode for when receiving them + // through a prepared statement. If a type appears in this list, it + // must also be implemented in binaryDecode in encode.go. + case oid.T_bytea: + fallthrough + case oid.T_int8: + fallthrough + case oid.T_int4: + fallthrough + case oid.T_int2: + fallthrough + case oid.T_uuid: + colFmts[i] = formatBinary + allText = false + + default: + allBinary = false + } + } + + if allBinary { + return colFmts, colFmtDataAllBinary + } else if allText { + return colFmts, colFmtDataAllText + } else { + colFmtData = make([]byte, 2+len(colFmts)*2) + binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts))) + for i, v := range colFmts { + binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v)) + } + return colFmts, colFmtData + } +} + +func (cn *conn) prepareTo(q, stmtName string) *stmt { + st := &stmt{cn: cn, name: stmtName} + + b := cn.writeBuf('P') + b.string(st.name) + b.string(q) + b.int16(0) + + b.next('D') + b.byte('S') + b.string(st.name) + + b.next('S') + cn.send(b) + + cn.readParseResponse() + st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse() + st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult) + cn.readReadyForQuery() + return st +} + +func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { + if err := cn.err.get(); err != nil { + return nil, err + } + defer cn.errRecover(&err) + + if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") { + s, err := cn.prepareCopyIn(q) + if err == nil { + cn.inCopy = true + } + return s, err + } + return cn.prepareTo(q, cn.gname()), nil +} + +func (cn *conn) Close() (err error) { + // Skip cn.bad return here because we always want to close a connection. + defer cn.errRecover(&err) + + // Ensure that cn.c.Close is always run. Since error handling is done with + // panics and cn.errRecover, the Close must be in a defer. + defer func() { + cerr := cn.c.Close() + if err == nil { + err = cerr + } + }() + + // Don't go through send(); ListenerConn relies on us not scribbling on the + // scratch buffer of this connection. + return cn.sendSimpleMessage('X') +} + +// Implement the "Queryer" interface +func (cn *conn) Query(query string, args []driver.Value) (driver.Rows, error) { + return cn.query(query, args) +} + +func (cn *conn) query(query string, args []driver.Value) (_ *rows, err error) { + if err := cn.err.get(); err != nil { + return nil, err + } + if cn.inCopy { + return nil, errCopyInProgress + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleQuery" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + return cn.simpleQuery(query) + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + rows := &rows{cn: cn} + rows.rowsHeader = cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + return rows, nil + } + st := cn.prepareTo(query, "") + st.exec(args) + return &rows{ + cn: cn, + rowsHeader: st.rowsHeader, + }, nil +} + +// Implement the optional "Execer" interface for one-shot queries +func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) { + if err := cn.err.get(); err != nil { + return nil, err + } + defer cn.errRecover(&err) + + // Check to see if we can use the "simpleExec" interface, which is + // *much* faster than going through prepare/exec + if len(args) == 0 { + // ignore commandTag, our caller doesn't care + r, _, err := cn.simpleExec(query) + return r, err + } + + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) + + cn.readParseResponse() + cn.readBindResponse() + cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + res, _, err = cn.readExecuteResponse("Execute") + return res, err + } + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st := cn.prepareTo(query, "") + r, err := st.Exec(args) + if err != nil { + panic(err) + } + return r, err +} + +type safeRetryError struct { + Err error +} + +func (se *safeRetryError) Error() string { + return se.Err.Error() +} + +func (cn *conn) send(m *writeBuf) { + n, err := cn.c.Write(m.wrap()) + if err != nil { + if n == 0 { + err = &safeRetryError{Err: err} + } + panic(err) + } +} + +func (cn *conn) sendStartupPacket(m *writeBuf) error { + _, err := cn.c.Write((m.wrap())[1:]) + return err +} + +// Send a message of type typ to the server on the other end of cn. The +// message should have no payload. This method does not use the scratch +// buffer. +func (cn *conn) sendSimpleMessage(typ byte) (err error) { + _, err = cn.c.Write([]byte{typ, '\x00', '\x00', '\x00', '\x04'}) + return err +} + +// saveMessage memorizes a message and its buffer in the conn struct. +// recvMessage will then return these values on the next call to it. This +// method is useful in cases where you have to see what the next message is +// going to be (e.g. to see whether it's an error or not) but you can't handle +// the message yourself. +func (cn *conn) saveMessage(typ byte, buf *readBuf) { + if cn.saveMessageType != 0 { + cn.err.set(driver.ErrBadConn) + errorf("unexpected saveMessageType %d", cn.saveMessageType) + } + cn.saveMessageType = typ + cn.saveMessageBuffer = *buf +} + +// recvMessage receives any message from the backend, or returns an error if +// a problem occurred while reading the message. +func (cn *conn) recvMessage(r *readBuf) (byte, error) { + // workaround for a QueryRow bug, see exec + if cn.saveMessageType != 0 { + t := cn.saveMessageType + *r = cn.saveMessageBuffer + cn.saveMessageType = 0 + cn.saveMessageBuffer = nil + return t, nil + } + + x := cn.scratch[:5] + _, err := io.ReadFull(cn.buf, x) + if err != nil { + return 0, err + } + + // read the type and length of the message that follows + t := x[0] + n := int(binary.BigEndian.Uint32(x[1:])) - 4 + var y []byte + if n <= len(cn.scratch) { + y = cn.scratch[:n] + } else { + y = make([]byte, n) + } + _, err = io.ReadFull(cn.buf, y) + if err != nil { + return 0, err + } + *r = y + return t, nil +} + +// recv receives a message from the backend, but if an error happened while +// reading the message or the received message was an ErrorResponse, it panics. +// NoticeResponses are ignored. This function should generally be used only +// during the startup sequence. +func (cn *conn) recv() (t byte, r *readBuf) { + for { + var err error + r = &readBuf{} + t, err = cn.recvMessage(r) + if err != nil { + panic(err) + } + switch t { + case 'E': + panic(parseError(r)) + case 'N': + if n := cn.noticeHandler; n != nil { + n(parseError(r)) + } + case 'A': + if n := cn.notificationHandler; n != nil { + n(recvNotification(r)) + } + default: + return + } + } +} + +// recv1Buf is exactly equivalent to recv1, except it uses a buffer supplied by +// the caller to avoid an allocation. +func (cn *conn) recv1Buf(r *readBuf) byte { + for { + t, err := cn.recvMessage(r) + if err != nil { + panic(err) + } + + switch t { + case 'A': + if n := cn.notificationHandler; n != nil { + n(recvNotification(r)) + } + case 'N': + if n := cn.noticeHandler; n != nil { + n(parseError(r)) + } + case 'S': + cn.processParameterStatus(r) + default: + return t + } + } +} + +// recv1 receives a message from the backend, panicking if an error occurs +// while attempting to read it. All asynchronous messages are ignored, with +// the exception of ErrorResponse. +func (cn *conn) recv1() (t byte, r *readBuf) { + r = &readBuf{} + t = cn.recv1Buf(r) + return t, r +} + +func (cn *conn) ssl(o values) error { + upgrade, err := ssl(o) + if err != nil { + return err + } + + if upgrade == nil { + // Nothing to do + return nil + } + + w := cn.writeBuf(0) + w.int32(80877103) + if err = cn.sendStartupPacket(w); err != nil { + return err + } + + b := cn.scratch[:1] + _, err = io.ReadFull(cn.c, b) + if err != nil { + return err + } + + if b[0] != 'S' { + return ErrSSLNotSupported + } + + cn.c, err = upgrade(cn.c) + return err +} + +// isDriverSetting returns true iff a setting is purely for configuring the +// driver's options and should not be sent to the server in the connection +// startup packet. +func isDriverSetting(key string) bool { + switch key { + case "host", "port": + return true + case "password": + return true + case "sslmode", "sslcert", "sslkey", "sslrootcert", "sslinline", "sslsni": + return true + case "fallback_application_name": + return true + case "connect_timeout": + return true + case "disable_prepared_binary_result": + return true + case "binary_parameters": + return true + case "krbsrvname": + return true + case "krbspn": + return true + default: + return false + } +} + +func (cn *conn) startup(o values) { + w := cn.writeBuf(0) + w.int32(196608) + // Send the backend the name of the database we want to connect to, and the + // user we want to connect as. Additionally, we send over any run-time + // parameters potentially included in the connection string. If the server + // doesn't recognize any of them, it will reply with an error. + for k, v := range o { + if isDriverSetting(k) { + // skip options which can't be run-time parameters + continue + } + // The protocol requires us to supply the database name as "database" + // instead of "dbname". + if k == "dbname" { + k = "database" + } + w.string(k) + w.string(v) + } + w.string("") + if err := cn.sendStartupPacket(w); err != nil { + panic(err) + } + + for { + t, r := cn.recv() + switch t { + case 'K': + cn.processBackendKeyData(r) + case 'S': + cn.processParameterStatus(r) + case 'R': + cn.auth(r, o) + case 'Z': + cn.processReadyForQuery(r) + return + default: + errorf("unknown response for startup: %q", t) + } + } +} + +func (cn *conn) auth(r *readBuf, o values) { + switch code := r.int32(); code { + case 0: + // OK + case 3: + w := cn.writeBuf('p') + w.string(o["password"]) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 5: + s := string(r.next(4)) + w := cn.writeBuf('p') + w.string("md5" + md5s(md5s(o["password"]+o["user"])+s)) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 0 { + errorf("unexpected authentication response: %q", t) + } + case 7: // GSSAPI, startup + if newGss == nil { + errorf("kerberos error: no GSSAPI provider registered (import github.com/lib/pq/auth/kerberos if you need Kerberos support)") + } + cli, err := newGss() + if err != nil { + errorf("kerberos error: %s", err.Error()) + } + + var token []byte + + if spn, ok := o["krbspn"]; ok { + // Use the supplied SPN if provided.. + token, err = cli.GetInitTokenFromSpn(spn) + } else { + // Allow the kerberos service name to be overridden + service := "postgres" + if val, ok := o["krbsrvname"]; ok { + service = val + } + + token, err = cli.GetInitToken(o["host"], service) + } + + if err != nil { + errorf("failed to get Kerberos ticket: %q", err) + } + + w := cn.writeBuf('p') + w.bytes(token) + cn.send(w) + + // Store for GSSAPI continue message + cn.gss = cli + + case 8: // GSSAPI continue + + if cn.gss == nil { + errorf("GSSAPI protocol error") + } + + b := []byte(*r) + + done, tokOut, err := cn.gss.Continue(b) + if err == nil && !done { + w := cn.writeBuf('p') + w.bytes(tokOut) + cn.send(w) + } + + // Errors fall through and read the more detailed message + // from the server.. + + case 10: + sc := scram.NewClient(sha256.New, o["user"], o["password"]) + sc.Step(nil) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + scOut := sc.Out() + + w := cn.writeBuf('p') + w.string("SCRAM-SHA-256") + w.int32(len(scOut)) + w.bytes(scOut) + cn.send(w) + + t, r := cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 11 { + errorf("unexpected authentication response: %q", t) + } + + nextStep := r.next(len(*r)) + sc.Step(nextStep) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + + scOut = sc.Out() + w = cn.writeBuf('p') + w.bytes(scOut) + cn.send(w) + + t, r = cn.recv() + if t != 'R' { + errorf("unexpected password response: %q", t) + } + + if r.int32() != 12 { + errorf("unexpected authentication response: %q", t) + } + + nextStep = r.next(len(*r)) + sc.Step(nextStep) + if sc.Err() != nil { + errorf("SCRAM-SHA-256 error: %s", sc.Err().Error()) + } + + default: + errorf("unknown authentication response: %d", code) + } +} + +type format int + +const formatText format = 0 +const formatBinary format = 1 + +// One result-column format code with the value 1 (i.e. all binary). +var colFmtDataAllBinary = []byte{0, 1, 0, 1} + +// No result-column format codes (i.e. all text). +var colFmtDataAllText = []byte{0, 0} + +type stmt struct { + cn *conn + name string + rowsHeader + colFmtData []byte + paramTyps []oid.Oid + closed bool +} + +func (st *stmt) Close() (err error) { + if st.closed { + return nil + } + if err := st.cn.err.get(); err != nil { + return err + } + defer st.cn.errRecover(&err) + + w := st.cn.writeBuf('C') + w.byte('S') + w.string(st.name) + st.cn.send(w) + + st.cn.send(st.cn.writeBuf('S')) + + t, _ := st.cn.recv1() + if t != '3' { + st.cn.err.set(driver.ErrBadConn) + errorf("unexpected close response: %q", t) + } + st.closed = true + + t, r := st.cn.recv1() + if t != 'Z' { + st.cn.err.set(driver.ErrBadConn) + errorf("expected ready for query, but got: %q", t) + } + st.cn.processReadyForQuery(r) + + return nil +} + +func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { + return st.query(v) +} + +func (st *stmt) query(v []driver.Value) (r *rows, err error) { + if err := st.cn.err.get(); err != nil { + return nil, err + } + defer st.cn.errRecover(&err) + + st.exec(v) + return &rows{ + cn: st.cn, + rowsHeader: st.rowsHeader, + }, nil +} + +func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { + if err := st.cn.err.get(); err != nil { + return nil, err + } + defer st.cn.errRecover(&err) + + st.exec(v) + res, _, err = st.cn.readExecuteResponse("simple query") + return res, err +} + +func (st *stmt) exec(v []driver.Value) { + if len(v) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(v)) + } + if len(v) != len(st.paramTyps) { + errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps)) + } + + cn := st.cn + w := cn.writeBuf('B') + w.byte(0) // unnamed portal + w.string(st.name) + + if cn.binaryParameters { + cn.sendBinaryParameters(w, v) + } else { + w.int16(0) + w.int16(len(v)) + for i, x := range v { + if x == nil { + w.int32(-1) + } else { + b := encode(&cn.parameterStatus, x, st.paramTyps[i]) + w.int32(len(b)) + w.bytes(b) + } + } + } + w.bytes(st.colFmtData) + + w.next('E') + w.byte(0) + w.int32(0) + + w.next('S') + cn.send(w) + + cn.readBindResponse() + cn.postExecuteWorkaround() + +} + +func (st *stmt) NumInput() int { + return len(st.paramTyps) +} + +// parseComplete parses the "command tag" from a CommandComplete message, and +// returns the number of rows affected (if applicable) and a string +// identifying only the command that was executed, e.g. "ALTER TABLE". If the +// command tag could not be parsed, parseComplete panics. +func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { + commandsWithAffectedRows := []string{ + "SELECT ", + // INSERT is handled below + "UPDATE ", + "DELETE ", + "FETCH ", + "MOVE ", + "COPY ", + } + + var affectedRows *string + for _, tag := range commandsWithAffectedRows { + if strings.HasPrefix(commandTag, tag) { + t := commandTag[len(tag):] + affectedRows = &t + commandTag = tag[:len(tag)-1] + break + } + } + // INSERT also includes the oid of the inserted row in its command tag. + // Oids in user tables are deprecated, and the oid is only returned when + // exactly one row is inserted, so it's unlikely to be of value to any + // real-world application and we can ignore it. + if affectedRows == nil && strings.HasPrefix(commandTag, "INSERT ") { + parts := strings.Split(commandTag, " ") + if len(parts) != 3 { + cn.err.set(driver.ErrBadConn) + errorf("unexpected INSERT command tag %s", commandTag) + } + affectedRows = &parts[len(parts)-1] + commandTag = "INSERT" + } + // There should be no affected rows attached to the tag, just return it + if affectedRows == nil { + return driver.RowsAffected(0), commandTag + } + n, err := strconv.ParseInt(*affectedRows, 10, 64) + if err != nil { + cn.err.set(driver.ErrBadConn) + errorf("could not parse commandTag: %s", err) + } + return driver.RowsAffected(n), commandTag +} + +type rowsHeader struct { + colNames []string + colTyps []fieldDesc + colFmts []format +} + +type rows struct { + cn *conn + finish func() + rowsHeader + done bool + rb readBuf + result driver.Result + tag string + + next *rowsHeader +} + +func (rs *rows) Close() error { + if finish := rs.finish; finish != nil { + defer finish() + } + // no need to look at cn.bad as Next() will + for { + err := rs.Next(nil) + switch err { + case nil: + case io.EOF: + // rs.Next can return io.EOF on both 'Z' (ready for query) and 'T' (row + // description, used with HasNextResultSet). We need to fetch messages until + // we hit a 'Z', which is done by waiting for done to be set. + if rs.done { + return nil + } + default: + return err + } + } +} + +func (rs *rows) Columns() []string { + return rs.colNames +} + +func (rs *rows) Result() driver.Result { + if rs.result == nil { + return emptyRows + } + return rs.result +} + +func (rs *rows) Tag() string { + return rs.tag +} + +func (rs *rows) Next(dest []driver.Value) (err error) { + if rs.done { + return io.EOF + } + + conn := rs.cn + if err := conn.err.getForNext(); err != nil { + return err + } + defer conn.errRecover(&err) + + for { + t := conn.recv1Buf(&rs.rb) + switch t { + case 'E': + err = parseError(&rs.rb) + case 'C', 'I': + if t == 'C' { + rs.result, rs.tag = conn.parseComplete(rs.rb.string()) + } + continue + case 'Z': + conn.processReadyForQuery(&rs.rb) + rs.done = true + if err != nil { + return err + } + return io.EOF + case 'D': + n := rs.rb.int16() + if err != nil { + conn.err.set(driver.ErrBadConn) + errorf("unexpected DataRow after error %s", err) + } + if n < len(dest) { + dest = dest[:n] + } + for i := range dest { + l := rs.rb.int32() + if l == -1 { + dest[i] = nil + continue + } + dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i].OID, rs.colFmts[i]) + } + return + case 'T': + next := parsePortalRowDescribe(&rs.rb) + rs.next = &next + return io.EOF + default: + errorf("unexpected message after execute: %q", t) + } + } +} + +func (rs *rows) HasNextResultSet() bool { + hasNext := rs.next != nil && !rs.done + return hasNext +} + +func (rs *rows) NextResultSet() error { + if rs.next == nil { + return io.EOF + } + rs.rowsHeader = *rs.next + rs.next = nil + return nil +} + +// QuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to be +// used as part of an SQL statement. For example: +// +// tblname := "my_table" +// data := "my_data" +// quoted := pq.QuoteIdentifier(tblname) +// err := db.Exec(fmt.Sprintf("INSERT INTO %s VALUES ($1)", quoted), data) +// +// Any double quotes in name will be escaped. The quoted identifier will be +// case sensitive when used in a query. If the input string contains a zero +// byte, the result will be truncated immediately before it. +func QuoteIdentifier(name string) string { + end := strings.IndexRune(name, 0) + if end > -1 { + name = name[:end] + } + return `"` + strings.Replace(name, `"`, `""`, -1) + `"` +} + +// QuoteLiteral quotes a 'literal' (e.g. a parameter, often used to pass literal +// to DDL and other statements that do not accept parameters) to be used as part +// of an SQL statement. For example: +// +// exp_date := pq.QuoteLiteral("2023-01-05 15:00:00Z") +// err := db.Exec(fmt.Sprintf("CREATE ROLE my_user VALID UNTIL %s", exp_date)) +// +// Any single quotes in name will be escaped. Any backslashes (i.e. "\") will be +// replaced by two backslashes (i.e. "\\") and the C-style escape identifier +// that PostgreSQL provides ('E') will be prepended to the string. +func QuoteLiteral(literal string) string { + // This follows the PostgreSQL internal algorithm for handling quoted literals + // from libpq, which can be found in the "PQEscapeStringInternal" function, + // which is found in the libpq/fe-exec.c source file: + // https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/interfaces/libpq/fe-exec.c + // + // substitute any single-quotes (') with two single-quotes ('') + literal = strings.Replace(literal, `'`, `''`, -1) + // determine if the string has any backslashes (\) in it. + // if it does, replace any backslashes (\) with two backslashes (\\) + // then, we need to wrap the entire string with a PostgreSQL + // C-style escape. Per how "PQEscapeStringInternal" handles this case, we + // also add a space before the "E" + if strings.Contains(literal, `\`) { + literal = strings.Replace(literal, `\`, `\\`, -1) + literal = ` E'` + literal + `'` + } else { + // otherwise, we can just wrap the literal with a pair of single quotes + literal = `'` + literal + `'` + } + return literal +} + +func md5s(s string) string { + h := md5.New() + h.Write([]byte(s)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { + // Do one pass over the parameters to see if we're going to send any of + // them over in binary. If we are, create a paramFormats array at the + // same time. + var paramFormats []int + for i, x := range args { + _, ok := x.([]byte) + if ok { + if paramFormats == nil { + paramFormats = make([]int, len(args)) + } + paramFormats[i] = 1 + } + } + if paramFormats == nil { + b.int16(0) + } else { + b.int16(len(paramFormats)) + for _, x := range paramFormats { + b.int16(x) + } + } + + b.int16(len(args)) + for _, x := range args { + if x == nil { + b.int32(-1) + } else { + datum := binaryEncode(&cn.parameterStatus, x) + b.int32(len(datum)) + b.bytes(datum) + } + } +} + +func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) { + if len(args) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args)) + } + + b := cn.writeBuf('P') + b.byte(0) // unnamed statement + b.string(query) + b.int16(0) + + b.next('B') + b.int16(0) // unnamed portal and statement + cn.sendBinaryParameters(b, args) + b.bytes(colFmtDataAllText) + + b.next('D') + b.byte('P') + b.byte(0) // unnamed portal + + b.next('E') + b.byte(0) + b.int32(0) + + b.next('S') + cn.send(b) +} + +func (cn *conn) processParameterStatus(r *readBuf) { + var err error + + param := r.string() + switch param { + case "server_version": + var major1 int + var major2 int + _, err = fmt.Sscanf(r.string(), "%d.%d", &major1, &major2) + if err == nil { + cn.parameterStatus.serverVersion = major1*10000 + major2*100 + } + + case "TimeZone": + cn.parameterStatus.currentLocation, err = time.LoadLocation(r.string()) + if err != nil { + cn.parameterStatus.currentLocation = nil + } + + default: + // ignore + } +} + +func (cn *conn) processReadyForQuery(r *readBuf) { + cn.txnStatus = transactionStatus(r.byte()) +} + +func (cn *conn) readReadyForQuery() { + t, r := cn.recv1() + switch t { + case 'Z': + cn.processReadyForQuery(r) + return + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected message %q; expected ReadyForQuery", t) + } +} + +func (cn *conn) processBackendKeyData(r *readBuf) { + cn.processID = r.int32() + cn.secretKey = r.int32() +} + +func (cn *conn) readParseResponse() { + t, r := cn.recv1() + switch t { + case '1': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected Parse response %q", t) + } +} + +func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []fieldDesc) { + for { + t, r := cn.recv1() + switch t { + case 't': + nparams := r.int16() + paramTyps = make([]oid.Oid, nparams) + for i := range paramTyps { + paramTyps[i] = r.oid() + } + case 'n': + return paramTyps, nil, nil + case 'T': + colNames, colTyps = parseStatementRowDescribe(r) + return paramTyps, colNames, colTyps + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected Describe statement response %q", t) + } + } +} + +func (cn *conn) readPortalDescribeResponse() rowsHeader { + t, r := cn.recv1() + switch t { + case 'T': + return parsePortalRowDescribe(r) + case 'n': + return rowsHeader{} + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected Describe response %q", t) + } + panic("not reached") +} + +func (cn *conn) readBindResponse() { + t, r := cn.recv1() + switch t { + case '2': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected Bind response %q", t) + } +} + +func (cn *conn) postExecuteWorkaround() { + // Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores + // any errors from rows.Next, which masks errors that happened during the + // execution of the query. To avoid the problem in common cases, we wait + // here for one more message from the database. If it's not an error the + // query will likely succeed (or perhaps has already, if it's a + // CommandComplete), so we push the message into the conn struct; recv1 + // will return it as the next message for rows.Next or rows.Close. + // However, if it's an error, we wait until ReadyForQuery and then return + // the error to our caller. + for { + t, r := cn.recv1() + switch t { + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + case 'C', 'D', 'I': + // the query didn't fail, but we can't process this message + cn.saveMessage(t, r) + return + default: + cn.err.set(driver.ErrBadConn) + errorf("unexpected message during extended query execution: %q", t) + } + } +} + +// Only for Exec(), since we ignore the returned data +func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) { + for { + t, r := cn.recv1() + switch t { + case 'C': + if err != nil { + cn.err.set(driver.ErrBadConn) + errorf("unexpected CommandComplete after error %s", err) + } + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + if res == nil && err == nil { + err = errUnexpectedReady + } + return res, commandTag, err + case 'E': + err = parseError(r) + case 'T', 'D', 'I': + if err != nil { + cn.err.set(driver.ErrBadConn) + errorf("unexpected %q after error %s", t, err) + } + if t == 'I' { + res = emptyRows + } + // ignore any results + default: + cn.err.set(driver.ErrBadConn) + errorf("unknown %s response: %q", protocolState, t) + } + } +} + +func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []fieldDesc) { + n := r.int16() + colNames = make([]string, n) + colTyps = make([]fieldDesc, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i].OID = r.oid() + colTyps[i].Len = r.int16() + colTyps[i].Mod = r.int32() + // format code not known when describing a statement; always 0 + r.next(2) + } + return +} + +func parsePortalRowDescribe(r *readBuf) rowsHeader { + n := r.int16() + colNames := make([]string, n) + colFmts := make([]format, n) + colTyps := make([]fieldDesc, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i].OID = r.oid() + colTyps[i].Len = r.int16() + colTyps[i].Mod = r.int32() + colFmts[i] = format(r.int16()) + } + return rowsHeader{ + colNames: colNames, + colFmts: colFmts, + colTyps: colTyps, + } +} + +// parseEnviron tries to mimic some of libpq's environment handling +// +// To ease testing, it does not directly reference os.Environ, but is +// designed to accept its output. +// +// Environment-set connection information is intended to have a higher +// precedence than a library default but lower than any explicitly +// passed information (such as in the URL or connection string). +func parseEnviron(env []string) (out map[string]string) { + out = make(map[string]string) + + for _, v := range env { + parts := strings.SplitN(v, "=", 2) + + accrue := func(keyname string) { + out[keyname] = parts[1] + } + unsupported := func() { + panic(fmt.Sprintf("setting %v not supported", parts[0])) + } + + // The order of these is the same as is seen in the + // PostgreSQL 9.1 manual. Unsupported but well-defined + // keys cause a panic; these should be unset prior to + // execution. Options which pq expects to be set to a + // certain value are allowed, but must be set to that + // value if present (they can, of course, be absent). + switch parts[0] { + case "PGHOST": + accrue("host") + case "PGHOSTADDR": + unsupported() + case "PGPORT": + accrue("port") + case "PGDATABASE": + accrue("dbname") + case "PGUSER": + accrue("user") + case "PGPASSWORD": + accrue("password") + case "PGSERVICE", "PGSERVICEFILE", "PGREALM": + unsupported() + case "PGOPTIONS": + accrue("options") + case "PGAPPNAME": + accrue("application_name") + case "PGSSLMODE": + accrue("sslmode") + case "PGSSLCERT": + accrue("sslcert") + case "PGSSLKEY": + accrue("sslkey") + case "PGSSLROOTCERT": + accrue("sslrootcert") + case "PGSSLSNI": + accrue("sslsni") + case "PGREQUIRESSL", "PGSSLCRL": + unsupported() + case "PGREQUIREPEER": + unsupported() + case "PGKRBSRVNAME", "PGGSSLIB": + unsupported() + case "PGCONNECT_TIMEOUT": + accrue("connect_timeout") + case "PGCLIENTENCODING": + accrue("client_encoding") + case "PGDATESTYLE": + accrue("datestyle") + case "PGTZ": + accrue("timezone") + case "PGGEQO": + accrue("geqo") + case "PGSYSCONFDIR", "PGLOCALEDIR": + unsupported() + } + } + + return out +} + +// isUTF8 returns whether name is a fuzzy variation of the string "UTF-8". +func isUTF8(name string) bool { + // Recognize all sorts of silly things as "UTF-8", like Postgres does + s := strings.Map(alnumLowerASCII, name) + return s == "utf8" || s == "unicode" +} + +func alnumLowerASCII(ch rune) rune { + if 'A' <= ch && ch <= 'Z' { + return ch + ('a' - 'A') + } + if 'a' <= ch && ch <= 'z' || '0' <= ch && ch <= '9' { + return ch + } + return -1 // discard +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/conn_go18.go b/project/10-posts/src/vendor/github.com/lib/pq/conn_go18.go new file mode 100644 index 0000000..63d4ca6 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/conn_go18.go @@ -0,0 +1,247 @@ +package pq + +import ( + "context" + "database/sql" + "database/sql/driver" + "fmt" + "io" + "io/ioutil" + "time" +) + +const ( + watchCancelDialContextTimeout = time.Second * 10 +) + +// Implement the "QueryerContext" interface +func (cn *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + finish := cn.watchCancel(ctx) + r, err := cn.query(query, list) + if err != nil { + if finish != nil { + finish() + } + return nil, err + } + r.finish = finish + return r, nil +} + +// Implement the "ExecerContext" interface +func (cn *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + + return cn.Exec(query, list) +} + +// Implement the "ConnPrepareContext" interface +func (cn *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + return cn.Prepare(query) +} + +// Implement the "ConnBeginTx" interface +func (cn *conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { + var mode string + + switch sql.IsolationLevel(opts.Isolation) { + case sql.LevelDefault: + // Don't touch mode: use the server's default + case sql.LevelReadUncommitted: + mode = " ISOLATION LEVEL READ UNCOMMITTED" + case sql.LevelReadCommitted: + mode = " ISOLATION LEVEL READ COMMITTED" + case sql.LevelRepeatableRead: + mode = " ISOLATION LEVEL REPEATABLE READ" + case sql.LevelSerializable: + mode = " ISOLATION LEVEL SERIALIZABLE" + default: + return nil, fmt.Errorf("pq: isolation level not supported: %d", opts.Isolation) + } + + if opts.ReadOnly { + mode += " READ ONLY" + } else { + mode += " READ WRITE" + } + + tx, err := cn.begin(mode) + if err != nil { + return nil, err + } + cn.txnFinish = cn.watchCancel(ctx) + return tx, nil +} + +func (cn *conn) Ping(ctx context.Context) error { + if finish := cn.watchCancel(ctx); finish != nil { + defer finish() + } + rows, err := cn.simpleQuery(";") + if err != nil { + return driver.ErrBadConn // https://golang.org/pkg/database/sql/driver/#Pinger + } + rows.Close() + return nil +} + +func (cn *conn) watchCancel(ctx context.Context) func() { + if done := ctx.Done(); done != nil { + finished := make(chan struct{}, 1) + go func() { + select { + case <-done: + select { + case finished <- struct{}{}: + default: + // We raced with the finish func, let the next query handle this with the + // context. + return + } + + // Set the connection state to bad so it does not get reused. + cn.err.set(ctx.Err()) + + // At this point the function level context is canceled, + // so it must not be used for the additional network + // request to cancel the query. + // Create a new context to pass into the dial. + ctxCancel, cancel := context.WithTimeout(context.Background(), watchCancelDialContextTimeout) + defer cancel() + + _ = cn.cancel(ctxCancel) + case <-finished: + } + }() + return func() { + select { + case <-finished: + cn.err.set(ctx.Err()) + cn.Close() + case finished <- struct{}{}: + } + } + } + return nil +} + +func (cn *conn) cancel(ctx context.Context) error { + // Create a new values map (copy). This makes sure the connection created + // in this method cannot write to the same underlying data, which could + // cause a concurrent map write panic. This is necessary because cancel + // is called from a goroutine in watchCancel. + o := make(values) + for k, v := range cn.opts { + o[k] = v + } + + c, err := dial(ctx, cn.dialer, o) + if err != nil { + return err + } + defer c.Close() + + { + can := conn{ + c: c, + } + err = can.ssl(o) + if err != nil { + return err + } + + w := can.writeBuf(0) + w.int32(80877102) // cancel request code + w.int32(cn.processID) + w.int32(cn.secretKey) + + if err := can.sendStartupPacket(w); err != nil { + return err + } + } + + // Read until EOF to ensure that the server received the cancel. + { + _, err := io.Copy(ioutil.Discard, c) + return err + } +} + +// Implement the "StmtQueryContext" interface +func (st *stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + finish := st.watchCancel(ctx) + r, err := st.query(list) + if err != nil { + if finish != nil { + finish() + } + return nil, err + } + r.finish = finish + return r, nil +} + +// Implement the "StmtExecContext" interface +func (st *stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { + list := make([]driver.Value, len(args)) + for i, nv := range args { + list[i] = nv.Value + } + + if finish := st.watchCancel(ctx); finish != nil { + defer finish() + } + + return st.Exec(list) +} + +// watchCancel is implemented on stmt in order to not mark the parent conn as bad +func (st *stmt) watchCancel(ctx context.Context) func() { + if done := ctx.Done(); done != nil { + finished := make(chan struct{}) + go func() { + select { + case <-done: + // At this point the function level context is canceled, + // so it must not be used for the additional network + // request to cancel the query. + // Create a new context to pass into the dial. + ctxCancel, cancel := context.WithTimeout(context.Background(), watchCancelDialContextTimeout) + defer cancel() + + _ = st.cancel(ctxCancel) + finished <- struct{}{} + case <-finished: + } + }() + return func() { + select { + case <-finished: + case finished <- struct{}{}: + } + } + } + return nil +} + +func (st *stmt) cancel(ctx context.Context) error { + return st.cn.cancel(ctx) +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/connector.go b/project/10-posts/src/vendor/github.com/lib/pq/connector.go new file mode 100644 index 0000000..1145e12 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/connector.go @@ -0,0 +1,120 @@ +package pq + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "os" + "strings" +) + +// Connector represents a fixed configuration for the pq driver with a given +// name. Connector satisfies the database/sql/driver Connector interface and +// can be used to create any number of DB Conn's via the database/sql OpenDB +// function. +// +// See https://golang.org/pkg/database/sql/driver/#Connector. +// See https://golang.org/pkg/database/sql/#OpenDB. +type Connector struct { + opts values + dialer Dialer +} + +// Connect returns a connection to the database using the fixed configuration +// of this Connector. Context is not used. +func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) { + return c.open(ctx) +} + +// Dialer allows change the dialer used to open connections. +func (c *Connector) Dialer(dialer Dialer) { + c.dialer = dialer +} + +// Driver returns the underlying driver of this Connector. +func (c *Connector) Driver() driver.Driver { + return &Driver{} +} + +// NewConnector returns a connector for the pq driver in a fixed configuration +// with the given dsn. The returned connector can be used to create any number +// of equivalent Conn's. The returned connector is intended to be used with +// database/sql.OpenDB. +// +// See https://golang.org/pkg/database/sql/driver/#Connector. +// See https://golang.org/pkg/database/sql/#OpenDB. +func NewConnector(dsn string) (*Connector, error) { + var err error + o := make(values) + + // A number of defaults are applied here, in this order: + // + // * Very low precedence defaults applied in every situation + // * Environment variables + // * Explicitly passed connection information + o["host"] = "localhost" + o["port"] = "5432" + // N.B.: Extra float digits should be set to 3, but that breaks + // Postgres 8.4 and older, where the max is 2. + o["extra_float_digits"] = "2" + for k, v := range parseEnviron(os.Environ()) { + o[k] = v + } + + if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { + dsn, err = ParseURL(dsn) + if err != nil { + return nil, err + } + } + + if err := parseOpts(dsn, o); err != nil { + return nil, err + } + + // Use the "fallback" application name if necessary + if fallback, ok := o["fallback_application_name"]; ok { + if _, ok := o["application_name"]; !ok { + o["application_name"] = fallback + } + } + + // We can't work with any client_encoding other than UTF-8 currently. + // However, we have historically allowed the user to set it to UTF-8 + // explicitly, and there's no reason to break such programs, so allow that. + // Note that the "options" setting could also set client_encoding, but + // parsing its value is not worth it. Instead, we always explicitly send + // client_encoding as a separate run-time parameter, which should override + // anything set in options. + if enc, ok := o["client_encoding"]; ok && !isUTF8(enc) { + return nil, errors.New("client_encoding must be absent or 'UTF8'") + } + o["client_encoding"] = "UTF8" + // DateStyle needs a similar treatment. + if datestyle, ok := o["datestyle"]; ok { + if datestyle != "ISO, MDY" { + return nil, fmt.Errorf("setting datestyle must be absent or %v; got %v", "ISO, MDY", datestyle) + } + } else { + o["datestyle"] = "ISO, MDY" + } + + // If a user is not provided by any other means, the last + // resort is to use the current operating system provided user + // name. + if _, ok := o["user"]; !ok { + u, err := userCurrent() + if err != nil { + return nil, err + } + o["user"] = u + } + + // SSL is not necessary or supported over UNIX domain sockets + if network, _ := network(o); network == "unix" { + o["sslmode"] = "disable" + } + + return &Connector{opts: o, dialer: defaultDialer{}}, nil +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/copy.go b/project/10-posts/src/vendor/github.com/lib/pq/copy.go new file mode 100644 index 0000000..2f5c1ec --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/copy.go @@ -0,0 +1,341 @@ +package pq + +import ( + "context" + "database/sql/driver" + "encoding/binary" + "errors" + "fmt" + "sync" +) + +var ( + errCopyInClosed = errors.New("pq: copyin statement has already been closed") + errBinaryCopyNotSupported = errors.New("pq: only text format supported for COPY") + errCopyToNotSupported = errors.New("pq: COPY TO is not supported") + errCopyNotSupportedOutsideTxn = errors.New("pq: COPY is only allowed inside a transaction") + errCopyInProgress = errors.New("pq: COPY in progress") +) + +// CopyIn creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). The target table should be visible in search_path. +func CopyIn(table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +// CopyInSchema creates a COPY FROM statement which can be prepared with +// Tx.Prepare(). +func CopyInSchema(schema, table string, columns ...string) string { + stmt := "COPY " + QuoteIdentifier(schema) + "." + QuoteIdentifier(table) + " (" + for i, col := range columns { + if i != 0 { + stmt += ", " + } + stmt += QuoteIdentifier(col) + } + stmt += ") FROM STDIN" + return stmt +} + +type copyin struct { + cn *conn + buffer []byte + rowData chan []byte + done chan bool + + closed bool + + mu struct { + sync.Mutex + err error + driver.Result + } +} + +const ciBufferSize = 64 * 1024 + +// flush buffer before the buffer is filled up and needs reallocation +const ciBufferFlushSize = 63 * 1024 + +func (cn *conn) prepareCopyIn(q string) (_ driver.Stmt, err error) { + if !cn.isInTransaction() { + return nil, errCopyNotSupportedOutsideTxn + } + + ci := ©in{ + cn: cn, + buffer: make([]byte, 0, ciBufferSize), + rowData: make(chan []byte), + done: make(chan bool, 1), + } + // add CopyData identifier + 4 bytes for message length + ci.buffer = append(ci.buffer, 'd', 0, 0, 0, 0) + + b := cn.writeBuf('Q') + b.string(q) + cn.send(b) + +awaitCopyInResponse: + for { + t, r := cn.recv1() + switch t { + case 'G': + if r.byte() != 0 { + err = errBinaryCopyNotSupported + break awaitCopyInResponse + } + go ci.resploop() + return ci, nil + case 'H': + err = errCopyToNotSupported + break awaitCopyInResponse + case 'E': + err = parseError(r) + case 'Z': + if err == nil { + ci.setBad(driver.ErrBadConn) + errorf("unexpected ReadyForQuery in response to COPY") + } + cn.processReadyForQuery(r) + return nil, err + default: + ci.setBad(driver.ErrBadConn) + errorf("unknown response for copy query: %q", t) + } + } + + // something went wrong, abort COPY before we return + b = cn.writeBuf('f') + b.string(err.Error()) + cn.send(b) + + for { + t, r := cn.recv1() + switch t { + case 'c', 'C', 'E': + case 'Z': + // correctly aborted, we're done + cn.processReadyForQuery(r) + return nil, err + default: + ci.setBad(driver.ErrBadConn) + errorf("unknown response for CopyFail: %q", t) + } + } +} + +func (ci *copyin) flush(buf []byte) { + // set message length (without message identifier) + binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1)) + + _, err := ci.cn.c.Write(buf) + if err != nil { + panic(err) + } +} + +func (ci *copyin) resploop() { + for { + var r readBuf + t, err := ci.cn.recvMessage(&r) + if err != nil { + ci.setBad(driver.ErrBadConn) + ci.setError(err) + ci.done <- true + return + } + switch t { + case 'C': + // complete + res, _ := ci.cn.parseComplete(r.string()) + ci.setResult(res) + case 'N': + if n := ci.cn.noticeHandler; n != nil { + n(parseError(&r)) + } + case 'Z': + ci.cn.processReadyForQuery(&r) + ci.done <- true + return + case 'E': + err := parseError(&r) + ci.setError(err) + default: + ci.setBad(driver.ErrBadConn) + ci.setError(fmt.Errorf("unknown response during CopyIn: %q", t)) + ci.done <- true + return + } + } +} + +func (ci *copyin) setBad(err error) { + ci.cn.err.set(err) +} + +func (ci *copyin) getBad() error { + return ci.cn.err.get() +} + +func (ci *copyin) err() error { + ci.mu.Lock() + err := ci.mu.err + ci.mu.Unlock() + return err +} + +// setError() sets ci.err if one has not been set already. Caller must not be +// holding ci.Mutex. +func (ci *copyin) setError(err error) { + ci.mu.Lock() + if ci.mu.err == nil { + ci.mu.err = err + } + ci.mu.Unlock() +} + +func (ci *copyin) setResult(result driver.Result) { + ci.mu.Lock() + ci.mu.Result = result + ci.mu.Unlock() +} + +func (ci *copyin) getResult() driver.Result { + ci.mu.Lock() + result := ci.mu.Result + ci.mu.Unlock() + if result == nil { + return driver.RowsAffected(0) + } + return result +} + +func (ci *copyin) NumInput() int { + return -1 +} + +func (ci *copyin) Query(v []driver.Value) (r driver.Rows, err error) { + return nil, ErrNotSupported +} + +// Exec inserts values into the COPY stream. The insert is asynchronous +// and Exec can return errors from previous Exec calls to the same +// COPY stmt. +// +// You need to call Exec(nil) to sync the COPY stream and to get any +// errors from pending data, since Stmt.Close() doesn't return errors +// to the user. +func (ci *copyin) Exec(v []driver.Value) (r driver.Result, err error) { + if ci.closed { + return nil, errCopyInClosed + } + + if err := ci.getBad(); err != nil { + return nil, err + } + defer ci.cn.errRecover(&err) + + if err := ci.err(); err != nil { + return nil, err + } + + if len(v) == 0 { + if err := ci.Close(); err != nil { + return driver.RowsAffected(0), err + } + + return ci.getResult(), nil + } + + numValues := len(v) + for i, value := range v { + ci.buffer = appendEncodedText(&ci.cn.parameterStatus, ci.buffer, value) + if i < numValues-1 { + ci.buffer = append(ci.buffer, '\t') + } + } + + ci.buffer = append(ci.buffer, '\n') + + if len(ci.buffer) > ciBufferFlushSize { + ci.flush(ci.buffer) + // reset buffer, keep bytes for message identifier and length + ci.buffer = ci.buffer[:5] + } + + return driver.RowsAffected(0), nil +} + +// CopyData inserts a raw string into the COPY stream. The insert is +// asynchronous and CopyData can return errors from previous CopyData calls to +// the same COPY stmt. +// +// You need to call Exec(nil) to sync the COPY stream and to get any +// errors from pending data, since Stmt.Close() doesn't return errors +// to the user. +func (ci *copyin) CopyData(ctx context.Context, line string) (r driver.Result, err error) { + if ci.closed { + return nil, errCopyInClosed + } + + if finish := ci.cn.watchCancel(ctx); finish != nil { + defer finish() + } + + if err := ci.getBad(); err != nil { + return nil, err + } + defer ci.cn.errRecover(&err) + + if err := ci.err(); err != nil { + return nil, err + } + + ci.buffer = append(ci.buffer, []byte(line)...) + ci.buffer = append(ci.buffer, '\n') + + if len(ci.buffer) > ciBufferFlushSize { + ci.flush(ci.buffer) + // reset buffer, keep bytes for message identifier and length + ci.buffer = ci.buffer[:5] + } + + return driver.RowsAffected(0), nil +} + +func (ci *copyin) Close() (err error) { + if ci.closed { // Don't do anything, we're already closed + return nil + } + ci.closed = true + + if err := ci.getBad(); err != nil { + return err + } + defer ci.cn.errRecover(&err) + + if len(ci.buffer) > 0 { + ci.flush(ci.buffer) + } + // Avoid touching the scratch buffer as resploop could be using it. + err = ci.cn.sendSimpleMessage('c') + if err != nil { + return err + } + + <-ci.done + ci.cn.inCopy = false + + if err := ci.err(); err != nil { + return err + } + return nil +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/doc.go b/project/10-posts/src/vendor/github.com/lib/pq/doc.go new file mode 100644 index 0000000..b571848 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/doc.go @@ -0,0 +1,268 @@ +/* +Package pq is a pure Go Postgres driver for the database/sql package. + +In most cases clients will use the database/sql package instead of +using this package directly. For example: + + import ( + "database/sql" + + _ "github.com/lib/pq" + ) + + func main() { + connStr := "user=pqgotest dbname=pqgotest sslmode=verify-full" + db, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatal(err) + } + + age := 21 + rows, err := db.Query("SELECT name FROM users WHERE age = $1", age) + … + } + +You can also connect to a database using a URL. For example: + + connStr := "postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full" + db, err := sql.Open("postgres", connStr) + + +Connection String Parameters + + +Similarly to libpq, when establishing a connection using pq you are expected to +supply a connection string containing zero or more parameters. +A subset of the connection parameters supported by libpq are also supported by pq. +Additionally, pq also lets you specify run-time parameters (such as search_path or work_mem) +directly in the connection string. This is different from libpq, which does not allow +run-time parameters in the connection string, instead requiring you to supply +them in the options parameter. + +For compatibility with libpq, the following special connection parameters are +supported: + + * dbname - The name of the database to connect to + * user - The user to sign in as + * password - The user's password + * host - The host to connect to. Values that start with / are for unix + domain sockets. (default is localhost) + * port - The port to bind to. (default is 5432) + * sslmode - Whether or not to use SSL (default is require, this is not + the default for libpq) + * fallback_application_name - An application_name to fall back to if one isn't provided. + * connect_timeout - Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + * sslcert - Cert file location. The file must contain PEM encoded data. + * sslkey - Key file location. The file must contain PEM encoded data. + * sslrootcert - The location of the root certificate file. The file + must contain PEM encoded data. + +Valid values for sslmode are: + + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + +See http://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING +for more information about connection string parameters. + +Use single quotes for values that contain whitespace: + + "user=pqgotest password='with spaces'" + +A backslash will escape the next character in values: + + "user=space\ man password='it\'s valid'" + +Note that the connection parameter client_encoding (which sets the +text encoding for the connection) may be set but must be "UTF8", +matching with the same rules as Postgres. It is an error to provide +any other value. + +In addition to the parameters listed above, any run-time parameter that can be +set at backend start time can be set in the connection string. For more +information, see +http://www.postgresql.org/docs/current/static/runtime-config.html. + +Most environment variables as specified at http://www.postgresql.org/docs/current/static/libpq-envars.html +supported by libpq are also supported by pq. If any of the environment +variables not supported by pq are set, pq will panic during connection +establishment. Environment variables have a lower precedence than explicitly +provided connection parameters. + +The pgpass mechanism as described in http://www.postgresql.org/docs/current/static/libpq-pgpass.html +is supported, but on Windows PGPASSFILE must be specified explicitly. + + +Queries + + +database/sql does not dictate any specific format for parameter +markers in query strings, and pq uses the Postgres-native ordinal markers, +as shown above. The same marker can be reused for the same parameter: + + rows, err := db.Query(`SELECT name FROM users WHERE favorite_fruit = $1 + OR age BETWEEN $2 AND $2 + 3`, "orange", 64) + +pq does not support the LastInsertId() method of the Result type in database/sql. +To return the identifier of an INSERT (or UPDATE or DELETE), use the Postgres +RETURNING clause with a standard Query or QueryRow call: + + var userid int + err := db.QueryRow(`INSERT INTO users(name, favorite_fruit, age) + VALUES('beatrice', 'starfruit', 93) RETURNING id`).Scan(&userid) + +For more details on RETURNING, see the Postgres documentation: + + http://www.postgresql.org/docs/current/static/sql-insert.html + http://www.postgresql.org/docs/current/static/sql-update.html + http://www.postgresql.org/docs/current/static/sql-delete.html + +For additional instructions on querying see the documentation for the database/sql package. + + +Data Types + + +Parameters pass through driver.DefaultParameterConverter before they are handled +by this package. When the binary_parameters connection option is enabled, +[]byte values are sent directly to the backend as data in binary format. + +This package returns the following types for values from the PostgreSQL backend: + + - integer types smallint, integer, and bigint are returned as int64 + - floating-point types real and double precision are returned as float64 + - character types char, varchar, and text are returned as string + - temporal types date, time, timetz, timestamp, and timestamptz are + returned as time.Time + - the boolean type is returned as bool + - the bytea type is returned as []byte + +All other types are returned directly from the backend as []byte values in text format. + + +Errors + + +pq may return errors of type *pq.Error which can be interrogated for error details: + + if err, ok := err.(*pq.Error); ok { + fmt.Println("pq error:", err.Code.Name()) + } + +See the pq.Error type for details. + + +Bulk imports + +You can perform bulk imports by preparing a statement returned by pq.CopyIn (or +pq.CopyInSchema) in an explicit transaction (sql.Tx). The returned statement +handle can then be repeatedly "executed" to copy data into the target table. +After all data has been processed you should call Exec() once with no arguments +to flush all buffered data. Any call to Exec() might return an error which +should be handled appropriately, but because of the internal buffering an error +returned by Exec() might not be related to the data passed in the call that +failed. + +CopyIn uses COPY FROM internally. It is not possible to COPY outside of an +explicit transaction in pq. + +Usage example: + + txn, err := db.Begin() + if err != nil { + log.Fatal(err) + } + + stmt, err := txn.Prepare(pq.CopyIn("users", "name", "age")) + if err != nil { + log.Fatal(err) + } + + for _, user := range users { + _, err = stmt.Exec(user.Name, int64(user.Age)) + if err != nil { + log.Fatal(err) + } + } + + _, err = stmt.Exec() + if err != nil { + log.Fatal(err) + } + + err = stmt.Close() + if err != nil { + log.Fatal(err) + } + + err = txn.Commit() + if err != nil { + log.Fatal(err) + } + + +Notifications + + +PostgreSQL supports a simple publish/subscribe model over database +connections. See http://www.postgresql.org/docs/current/static/sql-notify.html +for more information about the general mechanism. + +To start listening for notifications, you first have to open a new connection +to the database by calling NewListener. This connection can not be used for +anything other than LISTEN / NOTIFY. Calling Listen will open a "notification +channel"; once a notification channel is open, a notification generated on that +channel will effect a send on the Listener.Notify channel. A notification +channel will remain open until Unlisten is called, though connection loss might +result in some notifications being lost. To solve this problem, Listener sends +a nil pointer over the Notify channel any time the connection is re-established +following a connection loss. The application can get information about the +state of the underlying connection by setting an event callback in the call to +NewListener. + +A single Listener can safely be used from concurrent goroutines, which means +that there is often no need to create more than one Listener in your +application. However, a Listener is always connected to a single database, so +you will need to create a new Listener instance for every database you want to +receive notifications in. + +The channel name in both Listen and Unlisten is case sensitive, and can contain +any characters legal in an identifier (see +http://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS +for more information). Note that the channel name will be truncated to 63 +bytes by the PostgreSQL server. + +You can find a complete, working example of Listener usage at +https://godoc.org/github.com/lib/pq/example/listen. + + +Kerberos Support + + +If you need support for Kerberos authentication, add the following to your main +package: + + import "github.com/lib/pq/auth/kerberos" + + func init() { + pq.RegisterGSSProvider(func() (pq.Gss, error) { return kerberos.NewGSS() }) + } + +This package is in a separate module so that users who don't need Kerberos +don't have to download unnecessary dependencies. + +When imported, additional connection string parameters are supported: + + * krbsrvname - GSS (Kerberos) service name when constructing the + SPN (default is `postgres`). This will be combined with the host + to form the full SPN: `krbsrvname/host`. + * krbspn - GSS (Kerberos) SPN. This takes priority over + `krbsrvname` if present. +*/ +package pq diff --git a/project/10-posts/src/vendor/github.com/lib/pq/encode.go b/project/10-posts/src/vendor/github.com/lib/pq/encode.go new file mode 100644 index 0000000..bffe609 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/encode.go @@ -0,0 +1,632 @@ +package pq + +import ( + "bytes" + "database/sql/driver" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/lib/pq/oid" +) + +var time2400Regex = regexp.MustCompile(`^(24:00(?::00(?:\.0+)?)?)(?:[Z+-].*)?$`) + +func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte { + switch v := x.(type) { + case []byte: + return v + default: + return encode(parameterStatus, x, oid.T_unknown) + } +} + +func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(nil, v, 10) + case float64: + return strconv.AppendFloat(nil, v, 'f', -1, 64) + case []byte: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, v) + } + + return v + case string: + if pgtypOid == oid.T_bytea { + return encodeBytea(parameterStatus.serverVersion, []byte(v)) + } + + return []byte(v) + case bool: + return strconv.AppendBool(nil, v) + case time.Time: + return formatTs(v) + + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} { + switch f { + case formatBinary: + return binaryDecode(parameterStatus, s, typ) + case formatText: + return textDecode(parameterStatus, s, typ) + default: + panic("not reached") + } +} + +func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_bytea: + return s + case oid.T_int8: + return int64(binary.BigEndian.Uint64(s)) + case oid.T_int4: + return int64(int32(binary.BigEndian.Uint32(s))) + case oid.T_int2: + return int64(int16(binary.BigEndian.Uint16(s))) + case oid.T_uuid: + b, err := decodeUUIDBinary(s) + if err != nil { + panic(err) + } + return b + + default: + errorf("don't know how to decode binary parameter of type %d", uint32(typ)) + } + + panic("not reached") +} + +func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_char, oid.T_varchar, oid.T_text: + return string(s) + case oid.T_bytea: + b, err := parseBytea(s) + if err != nil { + errorf("%s", err) + } + return b + case oid.T_timestamptz: + return parseTs(parameterStatus.currentLocation, string(s)) + case oid.T_timestamp, oid.T_date: + return parseTs(nil, string(s)) + case oid.T_time: + return mustParse("15:04:05", typ, s) + case oid.T_timetz: + return mustParse("15:04:05-07", typ, s) + case oid.T_bool: + return s[0] == 't' + case oid.T_int8, oid.T_int4, oid.T_int2: + i, err := strconv.ParseInt(string(s), 10, 64) + if err != nil { + errorf("%s", err) + } + return i + case oid.T_float4, oid.T_float8: + // We always use 64 bit parsing, regardless of whether the input text is for + // a float4 or float8, because clients expect float64s for all float datatypes + // and returning a 32-bit parsed float64 produces lossy results. + f, err := strconv.ParseFloat(string(s), 64) + if err != nil { + errorf("%s", err) + } + return f + } + + return s +} + +// appendEncodedText encodes item in text format as required by COPY +// and appends to buf +func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface{}) []byte { + switch v := x.(type) { + case int64: + return strconv.AppendInt(buf, v, 10) + case float64: + return strconv.AppendFloat(buf, v, 'f', -1, 64) + case []byte: + encodedBytea := encodeBytea(parameterStatus.serverVersion, v) + return appendEscapedText(buf, string(encodedBytea)) + case string: + return appendEscapedText(buf, v) + case bool: + return strconv.AppendBool(buf, v) + case time.Time: + return append(buf, formatTs(v)...) + case nil: + return append(buf, "\\N"...) + default: + errorf("encode: unknown type for %T", v) + } + + panic("not reached") +} + +func appendEscapedText(buf []byte, text string) []byte { + escapeNeeded := false + startPos := 0 + var c byte + + // check if we need to escape + for i := 0; i < len(text); i++ { + c = text[i] + if c == '\\' || c == '\n' || c == '\r' || c == '\t' { + escapeNeeded = true + startPos = i + break + } + } + if !escapeNeeded { + return append(buf, text...) + } + + // copy till first char to escape, iterate the rest + result := append(buf, text[:startPos]...) + for i := startPos; i < len(text); i++ { + c = text[i] + switch c { + case '\\': + result = append(result, '\\', '\\') + case '\n': + result = append(result, '\\', 'n') + case '\r': + result = append(result, '\\', 'r') + case '\t': + result = append(result, '\\', 't') + default: + result = append(result, c) + } + } + return result +} + +func mustParse(f string, typ oid.Oid, s []byte) time.Time { + str := string(s) + + // Check for a minute and second offset in the timezone. + if typ == oid.T_timestamptz || typ == oid.T_timetz { + for i := 3; i <= 6; i += 3 { + if str[len(str)-i] == ':' { + f += ":00" + continue + } + break + } + } + + // Special case for 24:00 time. + // Unfortunately, golang does not parse 24:00 as a proper time. + // In this case, we want to try "round to the next day", to differentiate. + // As such, we find if the 24:00 time matches at the beginning; if so, + // we default it back to 00:00 but add a day later. + var is2400Time bool + switch typ { + case oid.T_timetz, oid.T_time: + if matches := time2400Regex.FindStringSubmatch(str); matches != nil { + // Concatenate timezone information at the back. + str = "00:00:00" + str[len(matches[1]):] + is2400Time = true + } + } + t, err := time.Parse(f, str) + if err != nil { + errorf("decode: %s", err) + } + if is2400Time { + t = t.Add(24 * time.Hour) + } + return t +} + +var errInvalidTimestamp = errors.New("invalid timestamp") + +type timestampParser struct { + err error +} + +func (p *timestampParser) expect(str string, char byte, pos int) { + if p.err != nil { + return + } + if pos+1 > len(str) { + p.err = errInvalidTimestamp + return + } + if c := str[pos]; c != char && p.err == nil { + p.err = fmt.Errorf("expected '%v' at position %v; got '%v'", char, pos, c) + } +} + +func (p *timestampParser) mustAtoi(str string, begin int, end int) int { + if p.err != nil { + return 0 + } + if begin < 0 || end < 0 || begin > end || end > len(str) { + p.err = errInvalidTimestamp + return 0 + } + result, err := strconv.Atoi(str[begin:end]) + if err != nil { + if p.err == nil { + p.err = fmt.Errorf("expected number; got '%v'", str) + } + return 0 + } + return result +} + +// The location cache caches the time zones typically used by the client. +type locationCache struct { + cache map[int]*time.Location + lock sync.Mutex +} + +// All connections share the same list of timezones. Benchmarking shows that +// about 5% speed could be gained by putting the cache in the connection and +// losing the mutex, at the cost of a small amount of memory and a somewhat +// significant increase in code complexity. +var globalLocationCache = newLocationCache() + +func newLocationCache() *locationCache { + return &locationCache{cache: make(map[int]*time.Location)} +} + +// Returns the cached timezone for the specified offset, creating and caching +// it if necessary. +func (c *locationCache) getLocation(offset int) *time.Location { + c.lock.Lock() + defer c.lock.Unlock() + + location, ok := c.cache[offset] + if !ok { + location = time.FixedZone("", offset) + c.cache[offset] = location + } + + return location +} + +var infinityTsEnabled = false +var infinityTsNegative time.Time +var infinityTsPositive time.Time + +const ( + infinityTsEnabledAlready = "pq: infinity timestamp enabled already" + infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive" +) + +// EnableInfinityTs controls the handling of Postgres' "-infinity" and +// "infinity" "timestamp"s. +// +// If EnableInfinityTs is not called, "-infinity" and "infinity" will return +// []byte("-infinity") and []byte("infinity") respectively, and potentially +// cause error "sql: Scan error on column index 0: unsupported driver -> Scan +// pair: []uint8 -> *time.Time", when scanning into a time.Time value. +// +// Once EnableInfinityTs has been called, all connections created using this +// driver will decode Postgres' "-infinity" and "infinity" for "timestamp", +// "timestamp with time zone" and "date" types to the predefined minimum and +// maximum times, respectively. When encoding time.Time values, any time which +// equals or precedes the predefined minimum time will be encoded to +// "-infinity". Any values at or past the maximum time will similarly be +// encoded to "infinity". +// +// If EnableInfinityTs is called with negative >= positive, it will panic. +// Calling EnableInfinityTs after a connection has been established results in +// undefined behavior. If EnableInfinityTs is called more than once, it will +// panic. +func EnableInfinityTs(negative time.Time, positive time.Time) { + if infinityTsEnabled { + panic(infinityTsEnabledAlready) + } + if !negative.Before(positive) { + panic(infinityTsNegativeMustBeSmaller) + } + infinityTsEnabled = true + infinityTsNegative = negative + infinityTsPositive = positive +} + +/* + * Testing might want to toggle infinityTsEnabled + */ +func disableInfinityTs() { + infinityTsEnabled = false +} + +// This is a time function specific to the Postgres default DateStyle +// setting ("ISO, MDY"), the only one we currently support. This +// accounts for the discrepancies between the parsing available with +// time.Parse and the Postgres date formatting quirks. +func parseTs(currentLocation *time.Location, str string) interface{} { + switch str { + case "-infinity": + if infinityTsEnabled { + return infinityTsNegative + } + return []byte(str) + case "infinity": + if infinityTsEnabled { + return infinityTsPositive + } + return []byte(str) + } + t, err := ParseTimestamp(currentLocation, str) + if err != nil { + panic(err) + } + return t +} + +// ParseTimestamp parses Postgres' text format. It returns a time.Time in +// currentLocation iff that time's offset agrees with the offset sent from the +// Postgres server. Otherwise, ParseTimestamp returns a time.Time with the +// fixed offset offset provided by the Postgres server. +func ParseTimestamp(currentLocation *time.Location, str string) (time.Time, error) { + p := timestampParser{} + + monSep := strings.IndexRune(str, '-') + // this is Gregorian year, not ISO Year + // In Gregorian system, the year 1 BC is followed by AD 1 + year := p.mustAtoi(str, 0, monSep) + daySep := monSep + 3 + month := p.mustAtoi(str, monSep+1, daySep) + p.expect(str, '-', daySep) + timeSep := daySep + 3 + day := p.mustAtoi(str, daySep+1, timeSep) + + minLen := monSep + len("01-01") + 1 + + isBC := strings.HasSuffix(str, " BC") + if isBC { + minLen += 3 + } + + var hour, minute, second int + if len(str) > minLen { + p.expect(str, ' ', timeSep) + minSep := timeSep + 3 + p.expect(str, ':', minSep) + hour = p.mustAtoi(str, timeSep+1, minSep) + secSep := minSep + 3 + p.expect(str, ':', secSep) + minute = p.mustAtoi(str, minSep+1, secSep) + secEnd := secSep + 3 + second = p.mustAtoi(str, secSep+1, secEnd) + } + remainderIdx := monSep + len("01-01 00:00:00") + 1 + // Three optional (but ordered) sections follow: the + // fractional seconds, the time zone offset, and the BC + // designation. We set them up here and adjust the other + // offsets if the preceding sections exist. + + nanoSec := 0 + tzOff := 0 + + if remainderIdx < len(str) && str[remainderIdx] == '.' { + fracStart := remainderIdx + 1 + fracOff := strings.IndexAny(str[fracStart:], "-+Z ") + if fracOff < 0 { + fracOff = len(str) - fracStart + } + fracSec := p.mustAtoi(str, fracStart, fracStart+fracOff) + nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff)))) + + remainderIdx += fracOff + 1 + } + if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') { + // time zone separator is always '-' or '+' or 'Z' (UTC is +00) + var tzSign int + switch c := str[tzStart]; c { + case '-': + tzSign = -1 + case '+': + tzSign = +1 + default: + return time.Time{}, fmt.Errorf("expected '-' or '+' at position %v; got %v", tzStart, c) + } + tzHours := p.mustAtoi(str, tzStart+1, tzStart+3) + remainderIdx += 3 + var tzMin, tzSec int + if remainderIdx < len(str) && str[remainderIdx] == ':' { + tzMin = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) + remainderIdx += 3 + } + if remainderIdx < len(str) && str[remainderIdx] == ':' { + tzSec = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) + remainderIdx += 3 + } + tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec) + } else if tzStart < len(str) && str[tzStart] == 'Z' { + // time zone Z separator indicates UTC is +00 + remainderIdx += 1 + } + + var isoYear int + + if isBC { + isoYear = 1 - year + remainderIdx += 3 + } else { + isoYear = year + } + if remainderIdx < len(str) { + return time.Time{}, fmt.Errorf("expected end of input, got %v", str[remainderIdx:]) + } + t := time.Date(isoYear, time.Month(month), day, + hour, minute, second, nanoSec, + globalLocationCache.getLocation(tzOff)) + + if currentLocation != nil { + // Set the location of the returned Time based on the session's + // TimeZone value, but only if the local time zone database agrees with + // the remote database on the offset. + lt := t.In(currentLocation) + _, newOff := lt.Zone() + if newOff == tzOff { + t = lt + } + } + + return t, p.err +} + +// formatTs formats t into a format postgres understands. +func formatTs(t time.Time) []byte { + if infinityTsEnabled { + // t <= -infinity : ! (t > -infinity) + if !t.After(infinityTsNegative) { + return []byte("-infinity") + } + // t >= infinity : ! (!t < infinity) + if !t.Before(infinityTsPositive) { + return []byte("infinity") + } + } + return FormatTimestamp(t) +} + +// FormatTimestamp formats t into Postgres' text format for timestamps. +func FormatTimestamp(t time.Time) []byte { + // Need to send dates before 0001 A.D. with " BC" suffix, instead of the + // minus sign preferred by Go. + // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on + bc := false + if t.Year() <= 0 { + // flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11" + t = t.AddDate((-t.Year())*2+1, 0, 0) + bc = true + } + b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00")) + + _, offset := t.Zone() + offset %= 60 + if offset != 0 { + // RFC3339Nano already printed the minus sign + if offset < 0 { + offset = -offset + } + + b = append(b, ':') + if offset < 10 { + b = append(b, '0') + } + b = strconv.AppendInt(b, int64(offset), 10) + } + + if bc { + b = append(b, " BC"...) + } + return b +} + +// Parse a bytea value received from the server. Both "hex" and the legacy +// "escape" format are supported. +func parseBytea(s []byte) (result []byte, err error) { + if len(s) >= 2 && bytes.Equal(s[:2], []byte("\\x")) { + // bytea_output = hex + s = s[2:] // trim off leading "\\x" + result = make([]byte, hex.DecodedLen(len(s))) + _, err := hex.Decode(result, s) + if err != nil { + return nil, err + } + } else { + // bytea_output = escape + for len(s) > 0 { + if s[0] == '\\' { + // escaped '\\' + if len(s) >= 2 && s[1] == '\\' { + result = append(result, '\\') + s = s[2:] + continue + } + + // '\\' followed by an octal number + if len(s) < 4 { + return nil, fmt.Errorf("invalid bytea sequence %v", s) + } + r, err := strconv.ParseUint(string(s[1:4]), 8, 8) + if err != nil { + return nil, fmt.Errorf("could not parse bytea value: %s", err.Error()) + } + result = append(result, byte(r)) + s = s[4:] + } else { + // We hit an unescaped, raw byte. Try to read in as many as + // possible in one go. + i := bytes.IndexByte(s, '\\') + if i == -1 { + result = append(result, s...) + break + } + result = append(result, s[:i]...) + s = s[i:] + } + } + } + + return result, nil +} + +func encodeBytea(serverVersion int, v []byte) (result []byte) { + if serverVersion >= 90000 { + // Use the hex format if we know that the server supports it + result = make([]byte, 2+hex.EncodedLen(len(v))) + result[0] = '\\' + result[1] = 'x' + hex.Encode(result[2:], v) + } else { + // .. or resort to "escape" + for _, b := range v { + if b == '\\' { + result = append(result, '\\', '\\') + } else if b < 0x20 || b > 0x7e { + result = append(result, []byte(fmt.Sprintf("\\%03o", b))...) + } else { + result = append(result, b) + } + } + } + + return result +} + +// NullTime represents a time.Time that may be null. NullTime implements the +// sql.Scanner interface so it can be used as a scan destination, similar to +// sql.NullString. +type NullTime struct { + Time time.Time + Valid bool // Valid is true if Time is not NULL +} + +// Scan implements the Scanner interface. +func (nt *NullTime) Scan(value interface{}) error { + nt.Time, nt.Valid = value.(time.Time) + return nil +} + +// Value implements the driver Valuer interface. +func (nt NullTime) Value() (driver.Value, error) { + if !nt.Valid { + return nil, nil + } + return nt.Time, nil +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/error.go b/project/10-posts/src/vendor/github.com/lib/pq/error.go new file mode 100644 index 0000000..f67c5a5 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/error.go @@ -0,0 +1,523 @@ +package pq + +import ( + "database/sql/driver" + "fmt" + "io" + "net" + "runtime" +) + +// Error severities +const ( + Efatal = "FATAL" + Epanic = "PANIC" + Ewarning = "WARNING" + Enotice = "NOTICE" + Edebug = "DEBUG" + Einfo = "INFO" + Elog = "LOG" +) + +// Error represents an error communicating with the server. +// +// See http://www.postgresql.org/docs/current/static/protocol-error-fields.html for details of the fields +type Error struct { + Severity string + Code ErrorCode + Message string + Detail string + Hint string + Position string + InternalPosition string + InternalQuery string + Where string + Schema string + Table string + Column string + DataTypeName string + Constraint string + File string + Line string + Routine string +} + +// ErrorCode is a five-character error code. +type ErrorCode string + +// Name returns a more human friendly rendering of the error code, namely the +// "condition name". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Name() string { + return errorCodeNames[ec] +} + +// ErrorClass is only the class part of an error code. +type ErrorClass string + +// Name returns the condition name of an error class. It is equivalent to the +// condition name of the "standard" error code (i.e. the one having the last +// three characters "000"). +func (ec ErrorClass) Name() string { + return errorCodeNames[ErrorCode(ec+"000")] +} + +// Class returns the error class, e.g. "28". +// +// See http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html for +// details. +func (ec ErrorCode) Class() ErrorClass { + return ErrorClass(ec[0:2]) +} + +// errorCodeNames is a mapping between the five-character error codes and the +// human readable "condition names". It is derived from the list at +// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html +var errorCodeNames = map[ErrorCode]string{ + // Class 00 - Successful Completion + "00000": "successful_completion", + // Class 01 - Warning + "01000": "warning", + "0100C": "dynamic_result_sets_returned", + "01008": "implicit_zero_bit_padding", + "01003": "null_value_eliminated_in_set_function", + "01007": "privilege_not_granted", + "01006": "privilege_not_revoked", + "01004": "string_data_right_truncation", + "01P01": "deprecated_feature", + // Class 02 - No Data (this is also a warning class per the SQL standard) + "02000": "no_data", + "02001": "no_additional_dynamic_result_sets_returned", + // Class 03 - SQL Statement Not Yet Complete + "03000": "sql_statement_not_yet_complete", + // Class 08 - Connection Exception + "08000": "connection_exception", + "08003": "connection_does_not_exist", + "08006": "connection_failure", + "08001": "sqlclient_unable_to_establish_sqlconnection", + "08004": "sqlserver_rejected_establishment_of_sqlconnection", + "08007": "transaction_resolution_unknown", + "08P01": "protocol_violation", + // Class 09 - Triggered Action Exception + "09000": "triggered_action_exception", + // Class 0A - Feature Not Supported + "0A000": "feature_not_supported", + // Class 0B - Invalid Transaction Initiation + "0B000": "invalid_transaction_initiation", + // Class 0F - Locator Exception + "0F000": "locator_exception", + "0F001": "invalid_locator_specification", + // Class 0L - Invalid Grantor + "0L000": "invalid_grantor", + "0LP01": "invalid_grant_operation", + // Class 0P - Invalid Role Specification + "0P000": "invalid_role_specification", + // Class 0Z - Diagnostics Exception + "0Z000": "diagnostics_exception", + "0Z002": "stacked_diagnostics_accessed_without_active_handler", + // Class 20 - Case Not Found + "20000": "case_not_found", + // Class 21 - Cardinality Violation + "21000": "cardinality_violation", + // Class 22 - Data Exception + "22000": "data_exception", + "2202E": "array_subscript_error", + "22021": "character_not_in_repertoire", + "22008": "datetime_field_overflow", + "22012": "division_by_zero", + "22005": "error_in_assignment", + "2200B": "escape_character_conflict", + "22022": "indicator_overflow", + "22015": "interval_field_overflow", + "2201E": "invalid_argument_for_logarithm", + "22014": "invalid_argument_for_ntile_function", + "22016": "invalid_argument_for_nth_value_function", + "2201F": "invalid_argument_for_power_function", + "2201G": "invalid_argument_for_width_bucket_function", + "22018": "invalid_character_value_for_cast", + "22007": "invalid_datetime_format", + "22019": "invalid_escape_character", + "2200D": "invalid_escape_octet", + "22025": "invalid_escape_sequence", + "22P06": "nonstandard_use_of_escape_character", + "22010": "invalid_indicator_parameter_value", + "22023": "invalid_parameter_value", + "2201B": "invalid_regular_expression", + "2201W": "invalid_row_count_in_limit_clause", + "2201X": "invalid_row_count_in_result_offset_clause", + "22009": "invalid_time_zone_displacement_value", + "2200C": "invalid_use_of_escape_character", + "2200G": "most_specific_type_mismatch", + "22004": "null_value_not_allowed", + "22002": "null_value_no_indicator_parameter", + "22003": "numeric_value_out_of_range", + "2200H": "sequence_generator_limit_exceeded", + "22026": "string_data_length_mismatch", + "22001": "string_data_right_truncation", + "22011": "substring_error", + "22027": "trim_error", + "22024": "unterminated_c_string", + "2200F": "zero_length_character_string", + "22P01": "floating_point_exception", + "22P02": "invalid_text_representation", + "22P03": "invalid_binary_representation", + "22P04": "bad_copy_file_format", + "22P05": "untranslatable_character", + "2200L": "not_an_xml_document", + "2200M": "invalid_xml_document", + "2200N": "invalid_xml_content", + "2200S": "invalid_xml_comment", + "2200T": "invalid_xml_processing_instruction", + // Class 23 - Integrity Constraint Violation + "23000": "integrity_constraint_violation", + "23001": "restrict_violation", + "23502": "not_null_violation", + "23503": "foreign_key_violation", + "23505": "unique_violation", + "23514": "check_violation", + "23P01": "exclusion_violation", + // Class 24 - Invalid Cursor State + "24000": "invalid_cursor_state", + // Class 25 - Invalid Transaction State + "25000": "invalid_transaction_state", + "25001": "active_sql_transaction", + "25002": "branch_transaction_already_active", + "25008": "held_cursor_requires_same_isolation_level", + "25003": "inappropriate_access_mode_for_branch_transaction", + "25004": "inappropriate_isolation_level_for_branch_transaction", + "25005": "no_active_sql_transaction_for_branch_transaction", + "25006": "read_only_sql_transaction", + "25007": "schema_and_data_statement_mixing_not_supported", + "25P01": "no_active_sql_transaction", + "25P02": "in_failed_sql_transaction", + // Class 26 - Invalid SQL Statement Name + "26000": "invalid_sql_statement_name", + // Class 27 - Triggered Data Change Violation + "27000": "triggered_data_change_violation", + // Class 28 - Invalid Authorization Specification + "28000": "invalid_authorization_specification", + "28P01": "invalid_password", + // Class 2B - Dependent Privilege Descriptors Still Exist + "2B000": "dependent_privilege_descriptors_still_exist", + "2BP01": "dependent_objects_still_exist", + // Class 2D - Invalid Transaction Termination + "2D000": "invalid_transaction_termination", + // Class 2F - SQL Routine Exception + "2F000": "sql_routine_exception", + "2F005": "function_executed_no_return_statement", + "2F002": "modifying_sql_data_not_permitted", + "2F003": "prohibited_sql_statement_attempted", + "2F004": "reading_sql_data_not_permitted", + // Class 34 - Invalid Cursor Name + "34000": "invalid_cursor_name", + // Class 38 - External Routine Exception + "38000": "external_routine_exception", + "38001": "containing_sql_not_permitted", + "38002": "modifying_sql_data_not_permitted", + "38003": "prohibited_sql_statement_attempted", + "38004": "reading_sql_data_not_permitted", + // Class 39 - External Routine Invocation Exception + "39000": "external_routine_invocation_exception", + "39001": "invalid_sqlstate_returned", + "39004": "null_value_not_allowed", + "39P01": "trigger_protocol_violated", + "39P02": "srf_protocol_violated", + // Class 3B - Savepoint Exception + "3B000": "savepoint_exception", + "3B001": "invalid_savepoint_specification", + // Class 3D - Invalid Catalog Name + "3D000": "invalid_catalog_name", + // Class 3F - Invalid Schema Name + "3F000": "invalid_schema_name", + // Class 40 - Transaction Rollback + "40000": "transaction_rollback", + "40002": "transaction_integrity_constraint_violation", + "40001": "serialization_failure", + "40003": "statement_completion_unknown", + "40P01": "deadlock_detected", + // Class 42 - Syntax Error or Access Rule Violation + "42000": "syntax_error_or_access_rule_violation", + "42601": "syntax_error", + "42501": "insufficient_privilege", + "42846": "cannot_coerce", + "42803": "grouping_error", + "42P20": "windowing_error", + "42P19": "invalid_recursion", + "42830": "invalid_foreign_key", + "42602": "invalid_name", + "42622": "name_too_long", + "42939": "reserved_name", + "42804": "datatype_mismatch", + "42P18": "indeterminate_datatype", + "42P21": "collation_mismatch", + "42P22": "indeterminate_collation", + "42809": "wrong_object_type", + "42703": "undefined_column", + "42883": "undefined_function", + "42P01": "undefined_table", + "42P02": "undefined_parameter", + "42704": "undefined_object", + "42701": "duplicate_column", + "42P03": "duplicate_cursor", + "42P04": "duplicate_database", + "42723": "duplicate_function", + "42P05": "duplicate_prepared_statement", + "42P06": "duplicate_schema", + "42P07": "duplicate_table", + "42712": "duplicate_alias", + "42710": "duplicate_object", + "42702": "ambiguous_column", + "42725": "ambiguous_function", + "42P08": "ambiguous_parameter", + "42P09": "ambiguous_alias", + "42P10": "invalid_column_reference", + "42611": "invalid_column_definition", + "42P11": "invalid_cursor_definition", + "42P12": "invalid_database_definition", + "42P13": "invalid_function_definition", + "42P14": "invalid_prepared_statement_definition", + "42P15": "invalid_schema_definition", + "42P16": "invalid_table_definition", + "42P17": "invalid_object_definition", + // Class 44 - WITH CHECK OPTION Violation + "44000": "with_check_option_violation", + // Class 53 - Insufficient Resources + "53000": "insufficient_resources", + "53100": "disk_full", + "53200": "out_of_memory", + "53300": "too_many_connections", + "53400": "configuration_limit_exceeded", + // Class 54 - Program Limit Exceeded + "54000": "program_limit_exceeded", + "54001": "statement_too_complex", + "54011": "too_many_columns", + "54023": "too_many_arguments", + // Class 55 - Object Not In Prerequisite State + "55000": "object_not_in_prerequisite_state", + "55006": "object_in_use", + "55P02": "cant_change_runtime_param", + "55P03": "lock_not_available", + // Class 57 - Operator Intervention + "57000": "operator_intervention", + "57014": "query_canceled", + "57P01": "admin_shutdown", + "57P02": "crash_shutdown", + "57P03": "cannot_connect_now", + "57P04": "database_dropped", + // Class 58 - System Error (errors external to PostgreSQL itself) + "58000": "system_error", + "58030": "io_error", + "58P01": "undefined_file", + "58P02": "duplicate_file", + // Class F0 - Configuration File Error + "F0000": "config_file_error", + "F0001": "lock_file_exists", + // Class HV - Foreign Data Wrapper Error (SQL/MED) + "HV000": "fdw_error", + "HV005": "fdw_column_name_not_found", + "HV002": "fdw_dynamic_parameter_value_needed", + "HV010": "fdw_function_sequence_error", + "HV021": "fdw_inconsistent_descriptor_information", + "HV024": "fdw_invalid_attribute_value", + "HV007": "fdw_invalid_column_name", + "HV008": "fdw_invalid_column_number", + "HV004": "fdw_invalid_data_type", + "HV006": "fdw_invalid_data_type_descriptors", + "HV091": "fdw_invalid_descriptor_field_identifier", + "HV00B": "fdw_invalid_handle", + "HV00C": "fdw_invalid_option_index", + "HV00D": "fdw_invalid_option_name", + "HV090": "fdw_invalid_string_length_or_buffer_length", + "HV00A": "fdw_invalid_string_format", + "HV009": "fdw_invalid_use_of_null_pointer", + "HV014": "fdw_too_many_handles", + "HV001": "fdw_out_of_memory", + "HV00P": "fdw_no_schemas", + "HV00J": "fdw_option_name_not_found", + "HV00K": "fdw_reply_handle", + "HV00Q": "fdw_schema_not_found", + "HV00R": "fdw_table_not_found", + "HV00L": "fdw_unable_to_create_execution", + "HV00M": "fdw_unable_to_create_reply", + "HV00N": "fdw_unable_to_establish_connection", + // Class P0 - PL/pgSQL Error + "P0000": "plpgsql_error", + "P0001": "raise_exception", + "P0002": "no_data_found", + "P0003": "too_many_rows", + // Class XX - Internal Error + "XX000": "internal_error", + "XX001": "data_corrupted", + "XX002": "index_corrupted", +} + +func parseError(r *readBuf) *Error { + err := new(Error) + for t := r.byte(); t != 0; t = r.byte() { + msg := r.string() + switch t { + case 'S': + err.Severity = msg + case 'C': + err.Code = ErrorCode(msg) + case 'M': + err.Message = msg + case 'D': + err.Detail = msg + case 'H': + err.Hint = msg + case 'P': + err.Position = msg + case 'p': + err.InternalPosition = msg + case 'q': + err.InternalQuery = msg + case 'W': + err.Where = msg + case 's': + err.Schema = msg + case 't': + err.Table = msg + case 'c': + err.Column = msg + case 'd': + err.DataTypeName = msg + case 'n': + err.Constraint = msg + case 'F': + err.File = msg + case 'L': + err.Line = msg + case 'R': + err.Routine = msg + } + } + return err +} + +// Fatal returns true if the Error Severity is fatal. +func (err *Error) Fatal() bool { + return err.Severity == Efatal +} + +// SQLState returns the SQLState of the error. +func (err *Error) SQLState() string { + return string(err.Code) +} + +// Get implements the legacy PGError interface. New code should use the fields +// of the Error struct directly. +func (err *Error) Get(k byte) (v string) { + switch k { + case 'S': + return err.Severity + case 'C': + return string(err.Code) + case 'M': + return err.Message + case 'D': + return err.Detail + case 'H': + return err.Hint + case 'P': + return err.Position + case 'p': + return err.InternalPosition + case 'q': + return err.InternalQuery + case 'W': + return err.Where + case 's': + return err.Schema + case 't': + return err.Table + case 'c': + return err.Column + case 'd': + return err.DataTypeName + case 'n': + return err.Constraint + case 'F': + return err.File + case 'L': + return err.Line + case 'R': + return err.Routine + } + return "" +} + +func (err *Error) Error() string { + return "pq: " + err.Message +} + +// PGError is an interface used by previous versions of pq. It is provided +// only to support legacy code. New code should use the Error type. +type PGError interface { + Error() string + Fatal() bool + Get(k byte) (v string) +} + +func errorf(s string, args ...interface{}) { + panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) +} + +// TODO(ainar-g) Rename to errorf after removing panics. +func fmterrorf(s string, args ...interface{}) error { + return fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)) +} + +func errRecoverNoErrBadConn(err *error) { + e := recover() + if e == nil { + // Do nothing + return + } + var ok bool + *err, ok = e.(error) + if !ok { + *err = fmt.Errorf("pq: unexpected error: %#v", e) + } +} + +func (cn *conn) errRecover(err *error) { + e := recover() + switch v := e.(type) { + case nil: + // Do nothing + case runtime.Error: + cn.err.set(driver.ErrBadConn) + panic(v) + case *Error: + if v.Fatal() { + *err = driver.ErrBadConn + } else { + *err = v + } + case *net.OpError: + cn.err.set(driver.ErrBadConn) + *err = v + case *safeRetryError: + cn.err.set(driver.ErrBadConn) + *err = driver.ErrBadConn + case error: + if v == io.EOF || v.Error() == "remote error: handshake failure" { + *err = driver.ErrBadConn + } else { + *err = v + } + + default: + cn.err.set(driver.ErrBadConn) + panic(fmt.Sprintf("unknown error: %#v", e)) + } + + // Any time we return ErrBadConn, we need to remember it since *Tx doesn't + // mark the connection bad in database/sql. + if *err == driver.ErrBadConn { + cn.err.set(driver.ErrBadConn) + } +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/krb.go b/project/10-posts/src/vendor/github.com/lib/pq/krb.go new file mode 100644 index 0000000..408ec01 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/krb.go @@ -0,0 +1,27 @@ +package pq + +// NewGSSFunc creates a GSS authentication provider, for use with +// RegisterGSSProvider. +type NewGSSFunc func() (GSS, error) + +var newGss NewGSSFunc + +// RegisterGSSProvider registers a GSS authentication provider. For example, if +// you need to use Kerberos to authenticate with your server, add this to your +// main package: +// +// import "github.com/lib/pq/auth/kerberos" +// +// func init() { +// pq.RegisterGSSProvider(func() (pq.GSS, error) { return kerberos.NewGSS() }) +// } +func RegisterGSSProvider(newGssArg NewGSSFunc) { + newGss = newGssArg +} + +// GSS provides GSSAPI authentication (e.g., Kerberos). +type GSS interface { + GetInitToken(host string, service string) ([]byte, error) + GetInitTokenFromSpn(spn string) ([]byte, error) + Continue(inToken []byte) (done bool, outToken []byte, err error) +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/notice.go b/project/10-posts/src/vendor/github.com/lib/pq/notice.go new file mode 100644 index 0000000..70ad122 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/notice.go @@ -0,0 +1,72 @@ +//go:build go1.10 +// +build go1.10 + +package pq + +import ( + "context" + "database/sql/driver" +) + +// NoticeHandler returns the notice handler on the given connection, if any. A +// runtime panic occurs if c is not a pq connection. This is rarely used +// directly, use ConnectorNoticeHandler and ConnectorWithNoticeHandler instead. +func NoticeHandler(c driver.Conn) func(*Error) { + return c.(*conn).noticeHandler +} + +// SetNoticeHandler sets the given notice handler on the given connection. A +// runtime panic occurs if c is not a pq connection. A nil handler may be used +// to unset it. This is rarely used directly, use ConnectorNoticeHandler and +// ConnectorWithNoticeHandler instead. +// +// Note: Notice handlers are executed synchronously by pq meaning commands +// won't continue to be processed until the handler returns. +func SetNoticeHandler(c driver.Conn, handler func(*Error)) { + c.(*conn).noticeHandler = handler +} + +// NoticeHandlerConnector wraps a regular connector and sets a notice handler +// on it. +type NoticeHandlerConnector struct { + driver.Connector + noticeHandler func(*Error) +} + +// Connect calls the underlying connector's connect method and then sets the +// notice handler. +func (n *NoticeHandlerConnector) Connect(ctx context.Context) (driver.Conn, error) { + c, err := n.Connector.Connect(ctx) + if err == nil { + SetNoticeHandler(c, n.noticeHandler) + } + return c, err +} + +// ConnectorNoticeHandler returns the currently set notice handler, if any. If +// the given connector is not a result of ConnectorWithNoticeHandler, nil is +// returned. +func ConnectorNoticeHandler(c driver.Connector) func(*Error) { + if c, ok := c.(*NoticeHandlerConnector); ok { + return c.noticeHandler + } + return nil +} + +// ConnectorWithNoticeHandler creates or sets the given handler for the given +// connector. If the given connector is a result of calling this function +// previously, it is simply set on the given connector and returned. Otherwise, +// this returns a new connector wrapping the given one and setting the notice +// handler. A nil notice handler may be used to unset it. +// +// The returned connector is intended to be used with database/sql.OpenDB. +// +// Note: Notice handlers are executed synchronously by pq meaning commands +// won't continue to be processed until the handler returns. +func ConnectorWithNoticeHandler(c driver.Connector, handler func(*Error)) *NoticeHandlerConnector { + if c, ok := c.(*NoticeHandlerConnector); ok { + c.noticeHandler = handler + return c + } + return &NoticeHandlerConnector{Connector: c, noticeHandler: handler} +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/notify.go b/project/10-posts/src/vendor/github.com/lib/pq/notify.go new file mode 100644 index 0000000..5c421fd --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/notify.go @@ -0,0 +1,858 @@ +package pq + +// Package pq is a pure Go Postgres driver for the database/sql package. +// This module contains support for Postgres LISTEN/NOTIFY. + +import ( + "context" + "database/sql/driver" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" +) + +// Notification represents a single notification from the database. +type Notification struct { + // Process ID (PID) of the notifying postgres backend. + BePid int + // Name of the channel the notification was sent on. + Channel string + // Payload, or the empty string if unspecified. + Extra string +} + +func recvNotification(r *readBuf) *Notification { + bePid := r.int32() + channel := r.string() + extra := r.string() + + return &Notification{bePid, channel, extra} +} + +// SetNotificationHandler sets the given notification handler on the given +// connection. A runtime panic occurs if c is not a pq connection. A nil handler +// may be used to unset it. +// +// Note: Notification handlers are executed synchronously by pq meaning commands +// won't continue to be processed until the handler returns. +func SetNotificationHandler(c driver.Conn, handler func(*Notification)) { + c.(*conn).notificationHandler = handler +} + +// NotificationHandlerConnector wraps a regular connector and sets a notification handler +// on it. +type NotificationHandlerConnector struct { + driver.Connector + notificationHandler func(*Notification) +} + +// Connect calls the underlying connector's connect method and then sets the +// notification handler. +func (n *NotificationHandlerConnector) Connect(ctx context.Context) (driver.Conn, error) { + c, err := n.Connector.Connect(ctx) + if err == nil { + SetNotificationHandler(c, n.notificationHandler) + } + return c, err +} + +// ConnectorNotificationHandler returns the currently set notification handler, if any. If +// the given connector is not a result of ConnectorWithNotificationHandler, nil is +// returned. +func ConnectorNotificationHandler(c driver.Connector) func(*Notification) { + if c, ok := c.(*NotificationHandlerConnector); ok { + return c.notificationHandler + } + return nil +} + +// ConnectorWithNotificationHandler creates or sets the given handler for the given +// connector. If the given connector is a result of calling this function +// previously, it is simply set on the given connector and returned. Otherwise, +// this returns a new connector wrapping the given one and setting the notification +// handler. A nil notification handler may be used to unset it. +// +// The returned connector is intended to be used with database/sql.OpenDB. +// +// Note: Notification handlers are executed synchronously by pq meaning commands +// won't continue to be processed until the handler returns. +func ConnectorWithNotificationHandler(c driver.Connector, handler func(*Notification)) *NotificationHandlerConnector { + if c, ok := c.(*NotificationHandlerConnector); ok { + c.notificationHandler = handler + return c + } + return &NotificationHandlerConnector{Connector: c, notificationHandler: handler} +} + +const ( + connStateIdle int32 = iota + connStateExpectResponse + connStateExpectReadyForQuery +) + +type message struct { + typ byte + err error +} + +var errListenerConnClosed = errors.New("pq: ListenerConn has been closed") + +// ListenerConn is a low-level interface for waiting for notifications. You +// should use Listener instead. +type ListenerConn struct { + // guards cn and err + connectionLock sync.Mutex + cn *conn + err error + + connState int32 + + // the sending goroutine will be holding this lock + senderLock sync.Mutex + + notificationChan chan<- *Notification + + replyChan chan message +} + +// NewListenerConn creates a new ListenerConn. Use NewListener instead. +func NewListenerConn(name string, notificationChan chan<- *Notification) (*ListenerConn, error) { + return newDialListenerConn(defaultDialer{}, name, notificationChan) +} + +func newDialListenerConn(d Dialer, name string, c chan<- *Notification) (*ListenerConn, error) { + cn, err := DialOpen(d, name) + if err != nil { + return nil, err + } + + l := &ListenerConn{ + cn: cn.(*conn), + notificationChan: c, + connState: connStateIdle, + replyChan: make(chan message, 2), + } + + go l.listenerConnMain() + + return l, nil +} + +// We can only allow one goroutine at a time to be running a query on the +// connection for various reasons, so the goroutine sending on the connection +// must be holding senderLock. +// +// Returns an error if an unrecoverable error has occurred and the ListenerConn +// should be abandoned. +func (l *ListenerConn) acquireSenderLock() error { + // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery + l.senderLock.Lock() + + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + if err != nil { + l.senderLock.Unlock() + return err + } + return nil +} + +func (l *ListenerConn) releaseSenderLock() { + l.senderLock.Unlock() +} + +// setState advances the protocol state to newState. Returns false if moving +// to that state from the current state is not allowed. +func (l *ListenerConn) setState(newState int32) bool { + var expectedState int32 + + switch newState { + case connStateIdle: + expectedState = connStateExpectReadyForQuery + case connStateExpectResponse: + expectedState = connStateIdle + case connStateExpectReadyForQuery: + expectedState = connStateExpectResponse + default: + panic(fmt.Sprintf("unexpected listenerConnState %d", newState)) + } + + return atomic.CompareAndSwapInt32(&l.connState, expectedState, newState) +} + +// Main logic is here: receive messages from the postgres backend, forward +// notifications and query replies and keep the internal state in sync with the +// protocol state. Returns when the connection has been lost, is about to go +// away or should be discarded because we couldn't agree on the state with the +// server backend. +func (l *ListenerConn) listenerConnLoop() (err error) { + defer errRecoverNoErrBadConn(&err) + + r := &readBuf{} + for { + t, err := l.cn.recvMessage(r) + if err != nil { + return err + } + + switch t { + case 'A': + // recvNotification copies all the data so we don't need to worry + // about the scratch buffer being overwritten. + l.notificationChan <- recvNotification(r) + + case 'T', 'D': + // only used by tests; ignore + + case 'E': + // We might receive an ErrorResponse even when not in a query; it + // is expected that the server will close the connection after + // that, but we should make sure that the error we display is the + // one from the stray ErrorResponse, not io.ErrUnexpectedEOF. + if !l.setState(connStateExpectReadyForQuery) { + return parseError(r) + } + l.replyChan <- message{t, parseError(r)} + + case 'C', 'I': + if !l.setState(connStateExpectReadyForQuery) { + // protocol out of sync + return fmt.Errorf("unexpected CommandComplete") + } + // ExecSimpleQuery doesn't need to know about this message + + case 'Z': + if !l.setState(connStateIdle) { + // protocol out of sync + return fmt.Errorf("unexpected ReadyForQuery") + } + l.replyChan <- message{t, nil} + + case 'S': + // ignore + case 'N': + if n := l.cn.noticeHandler; n != nil { + n(parseError(r)) + } + default: + return fmt.Errorf("unexpected message %q from server in listenerConnLoop", t) + } + } +} + +// This is the main routine for the goroutine receiving on the database +// connection. Most of the main logic is in listenerConnLoop. +func (l *ListenerConn) listenerConnMain() { + err := l.listenerConnLoop() + + // listenerConnLoop terminated; we're done, but we still have to clean up. + // Make sure nobody tries to start any new queries by making sure the err + // pointer is set. It is important that we do not overwrite its value; a + // connection could be closed by either this goroutine or one sending on + // the connection -- whoever closes the connection is assumed to have the + // more meaningful error message (as the other one will probably get + // net.errClosed), so that goroutine sets the error we expose while the + // other error is discarded. If the connection is lost while two + // goroutines are operating on the socket, it probably doesn't matter which + // error we expose so we don't try to do anything more complex. + l.connectionLock.Lock() + if l.err == nil { + l.err = err + } + l.cn.Close() + l.connectionLock.Unlock() + + // There might be a query in-flight; make sure nobody's waiting for a + // response to it, since there's not going to be one. + close(l.replyChan) + + // let the listener know we're done + close(l.notificationChan) + + // this ListenerConn is done +} + +// Listen sends a LISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Listen(channel string) (bool, error) { + return l.ExecSimpleQuery("LISTEN " + QuoteIdentifier(channel)) +} + +// Unlisten sends an UNLISTEN query to the server. See ExecSimpleQuery. +func (l *ListenerConn) Unlisten(channel string) (bool, error) { + return l.ExecSimpleQuery("UNLISTEN " + QuoteIdentifier(channel)) +} + +// UnlistenAll sends an `UNLISTEN *` query to the server. See ExecSimpleQuery. +func (l *ListenerConn) UnlistenAll() (bool, error) { + return l.ExecSimpleQuery("UNLISTEN *") +} + +// Ping the remote server to make sure it's alive. Non-nil error means the +// connection has failed and should be abandoned. +func (l *ListenerConn) Ping() error { + sent, err := l.ExecSimpleQuery("") + if !sent { + return err + } + if err != nil { + // shouldn't happen + panic(err) + } + return nil +} + +// Attempt to send a query on the connection. Returns an error if sending the +// query failed, and the caller should initiate closure of this connection. +// The caller must be holding senderLock (see acquireSenderLock and +// releaseSenderLock). +func (l *ListenerConn) sendSimpleQuery(q string) (err error) { + defer errRecoverNoErrBadConn(&err) + + // must set connection state before sending the query + if !l.setState(connStateExpectResponse) { + panic("two queries running at the same time") + } + + // Can't use l.cn.writeBuf here because it uses the scratch buffer which + // might get overwritten by listenerConnLoop. + b := &writeBuf{ + buf: []byte("Q\x00\x00\x00\x00"), + pos: 1, + } + b.string(q) + l.cn.send(b) + + return nil +} + +// ExecSimpleQuery executes a "simple query" (i.e. one with no bindable +// parameters) on the connection. The possible return values are: +// 1) "executed" is true; the query was executed to completion on the +// database server. If the query failed, err will be set to the error +// returned by the database, otherwise err will be nil. +// 2) If "executed" is false, the query could not be executed on the remote +// server. err will be non-nil. +// +// After a call to ExecSimpleQuery has returned an executed=false value, the +// connection has either been closed or will be closed shortly thereafter, and +// all subsequently executed queries will return an error. +func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { + if err = l.acquireSenderLock(); err != nil { + return false, err + } + defer l.releaseSenderLock() + + err = l.sendSimpleQuery(q) + if err != nil { + // We can't know what state the protocol is in, so we need to abandon + // this connection. + l.connectionLock.Lock() + // Set the error pointer if it hasn't been set already; see + // listenerConnMain. + if l.err == nil { + l.err = err + } + l.connectionLock.Unlock() + l.cn.c.Close() + return false, err + } + + // now we just wait for a reply.. + for { + m, ok := <-l.replyChan + if !ok { + // We lost the connection to server, don't bother waiting for a + // a response. err should have been set already. + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + return false, err + } + switch m.typ { + case 'Z': + // sanity check + if m.err != nil { + panic("m.err != nil") + } + // done; err might or might not be set + return true, err + + case 'E': + // sanity check + if m.err == nil { + panic("m.err == nil") + } + // server responded with an error; ReadyForQuery to follow + err = m.err + + default: + return false, fmt.Errorf("unknown response for simple query: %q", m.typ) + } + } +} + +// Close closes the connection. +func (l *ListenerConn) Close() error { + l.connectionLock.Lock() + if l.err != nil { + l.connectionLock.Unlock() + return errListenerConnClosed + } + l.err = errListenerConnClosed + l.connectionLock.Unlock() + // We can't send anything on the connection without holding senderLock. + // Simply close the net.Conn to wake up everyone operating on it. + return l.cn.c.Close() +} + +// Err returns the reason the connection was closed. It is not safe to call +// this function until l.Notify has been closed. +func (l *ListenerConn) Err() error { + return l.err +} + +var errListenerClosed = errors.New("pq: Listener has been closed") + +// ErrChannelAlreadyOpen is returned from Listen when a channel is already +// open. +var ErrChannelAlreadyOpen = errors.New("pq: channel is already open") + +// ErrChannelNotOpen is returned from Unlisten when a channel is not open. +var ErrChannelNotOpen = errors.New("pq: channel is not open") + +// ListenerEventType is an enumeration of listener event types. +type ListenerEventType int + +const ( + // ListenerEventConnected is emitted only when the database connection + // has been initially initialized. The err argument of the callback + // will always be nil. + ListenerEventConnected ListenerEventType = iota + + // ListenerEventDisconnected is emitted after a database connection has + // been lost, either because of an error or because Close has been + // called. The err argument will be set to the reason the database + // connection was lost. + ListenerEventDisconnected + + // ListenerEventReconnected is emitted after a database connection has + // been re-established after connection loss. The err argument of the + // callback will always be nil. After this event has been emitted, a + // nil pq.Notification is sent on the Listener.Notify channel. + ListenerEventReconnected + + // ListenerEventConnectionAttemptFailed is emitted after a connection + // to the database was attempted, but failed. The err argument will be + // set to an error describing why the connection attempt did not + // succeed. + ListenerEventConnectionAttemptFailed +) + +// EventCallbackType is the event callback type. See also ListenerEventType +// constants' documentation. +type EventCallbackType func(event ListenerEventType, err error) + +// Listener provides an interface for listening to notifications from a +// PostgreSQL database. For general usage information, see section +// "Notifications". +// +// Listener can safely be used from concurrently running goroutines. +type Listener struct { + // Channel for receiving notifications from the database. In some cases a + // nil value will be sent. See section "Notifications" above. + Notify chan *Notification + + name string + minReconnectInterval time.Duration + maxReconnectInterval time.Duration + dialer Dialer + eventCallback EventCallbackType + + lock sync.Mutex + isClosed bool + reconnectCond *sync.Cond + cn *ListenerConn + connNotificationChan <-chan *Notification + channels map[string]struct{} +} + +// NewListener creates a new database connection dedicated to LISTEN / NOTIFY. +// +// name should be set to a connection string to be used to establish the +// database connection (see section "Connection String Parameters" above). +// +// minReconnectInterval controls the duration to wait before trying to +// re-establish the database connection after connection loss. After each +// consecutive failure this interval is doubled, until maxReconnectInterval is +// reached. Successfully completing the connection establishment procedure +// resets the interval back to minReconnectInterval. +// +// The last parameter eventCallback can be set to a function which will be +// called by the Listener when the state of the underlying database connection +// changes. This callback will be called by the goroutine which dispatches the +// notifications over the Notify channel, so you should try to avoid doing +// potentially time-consuming operations from the callback. +func NewListener(name string, + minReconnectInterval time.Duration, + maxReconnectInterval time.Duration, + eventCallback EventCallbackType) *Listener { + return NewDialListener(defaultDialer{}, name, minReconnectInterval, maxReconnectInterval, eventCallback) +} + +// NewDialListener is like NewListener but it takes a Dialer. +func NewDialListener(d Dialer, + name string, + minReconnectInterval time.Duration, + maxReconnectInterval time.Duration, + eventCallback EventCallbackType) *Listener { + + l := &Listener{ + name: name, + minReconnectInterval: minReconnectInterval, + maxReconnectInterval: maxReconnectInterval, + dialer: d, + eventCallback: eventCallback, + + channels: make(map[string]struct{}), + + Notify: make(chan *Notification, 32), + } + l.reconnectCond = sync.NewCond(&l.lock) + + go l.listenerMain() + + return l +} + +// NotificationChannel returns the notification channel for this listener. +// This is the same channel as Notify, and will not be recreated during the +// life time of the Listener. +func (l *Listener) NotificationChannel() <-chan *Notification { + return l.Notify +} + +// Listen starts listening for notifications on a channel. Calls to this +// function will block until an acknowledgement has been received from the +// server. Note that Listener automatically re-establishes the connection +// after connection loss, so this function may block indefinitely if the +// connection can not be re-established. +// +// Listen will only fail in three conditions: +// 1) The channel is already open. The returned error will be +// ErrChannelAlreadyOpen. +// 2) The query was executed on the remote server, but PostgreSQL returned an +// error message in response to the query. The returned error will be a +// pq.Error containing the information the server supplied. +// 3) Close is called on the Listener before the request could be completed. +// +// The channel name is case-sensitive. +func (l *Listener) Listen(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // The server allows you to issue a LISTEN on a channel which is already + // open, but it seems useful to be able to detect this case to spot for + // mistakes in application logic. If the application genuinely does't + // care, it can check the exported error and ignore it. + _, exists := l.channels[channel] + if exists { + return ErrChannelAlreadyOpen + } + + if l.cn != nil { + // If gotResponse is true but error is set, the query was executed on + // the remote server, but resulted in an error. This should be + // relatively rare, so it's fine if we just pass the error to our + // caller. However, if gotResponse is false, we could not complete the + // query on the remote server and our underlying connection is about + // to go away, so we only add relname to l.channels, and wait for + // resync() to take care of the rest. + gotResponse, err := l.cn.Listen(channel) + if gotResponse && err != nil { + return err + } + } + + l.channels[channel] = struct{}{} + for l.cn == nil { + l.reconnectCond.Wait() + // we let go of the mutex for a while + if l.isClosed { + return errListenerClosed + } + } + + return nil +} + +// Unlisten removes a channel from the Listener's channel list. Returns +// ErrChannelNotOpen if the Listener is not listening on the specified channel. +// Returns immediately with no error if there is no connection. Note that you +// might still get notifications for this channel even after Unlisten has +// returned. +// +// The channel name is case-sensitive. +func (l *Listener) Unlisten(channel string) error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + // Similarly to LISTEN, this is not an error in Postgres, but it seems + // useful to distinguish from the normal conditions. + _, exists := l.channels[channel] + if !exists { + return ErrChannelNotOpen + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.Unlisten(channel) + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + delete(l.channels, channel) + return nil +} + +// UnlistenAll removes all channels from the Listener's channel list. Returns +// immediately with no error if there is no connection. Note that you might +// still get notifications for any of the deleted channels even after +// UnlistenAll has returned. +func (l *Listener) UnlistenAll() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + // Similarly to Listen (see comment in that function), the caller + // should only be bothered with an error if it came from the backend as + // a response to our query. + gotResponse, err := l.cn.UnlistenAll() + if gotResponse && err != nil { + return err + } + } + + // Don't bother waiting for resync if there's no connection. + l.channels = make(map[string]struct{}) + return nil +} + +// Ping the remote server to make sure it's alive. Non-nil return value means +// that there is no active connection. +func (l *Listener) Ping() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + if l.cn == nil { + return errors.New("no connection") + } + + return l.cn.Ping() +} + +// Clean up after losing the server connection. Returns l.cn.Err(), which +// should have the reason the connection was lost. +func (l *Listener) disconnectCleanup() error { + l.lock.Lock() + defer l.lock.Unlock() + + // sanity check; can't look at Err() until the channel has been closed + select { + case _, ok := <-l.connNotificationChan: + if ok { + panic("connNotificationChan not closed") + } + default: + panic("connNotificationChan not closed") + } + + err := l.cn.Err() + l.cn.Close() + l.cn = nil + return err +} + +// Synchronize the list of channels we want to be listening on with the server +// after the connection has been established. +func (l *Listener) resync(cn *ListenerConn, notificationChan <-chan *Notification) error { + doneChan := make(chan error) + go func(notificationChan <-chan *Notification) { + for channel := range l.channels { + // If we got a response, return that error to our caller as it's + // going to be more descriptive than cn.Err(). + gotResponse, err := cn.Listen(channel) + if gotResponse && err != nil { + doneChan <- err + return + } + + // If we couldn't reach the server, wait for notificationChan to + // close and then return the error message from the connection, as + // per ListenerConn's interface. + if err != nil { + for range notificationChan { + } + doneChan <- cn.Err() + return + } + } + doneChan <- nil + }(notificationChan) + + // Ignore notifications while synchronization is going on to avoid + // deadlocks. We have to send a nil notification over Notify anyway as + // we can't possibly know which notifications (if any) were lost while + // the connection was down, so there's no reason to try and process + // these messages at all. + for { + select { + case _, ok := <-notificationChan: + if !ok { + notificationChan = nil + } + + case err := <-doneChan: + return err + } + } +} + +// caller should NOT be holding l.lock +func (l *Listener) closed() bool { + l.lock.Lock() + defer l.lock.Unlock() + + return l.isClosed +} + +func (l *Listener) connect() error { + notificationChan := make(chan *Notification, 32) + cn, err := newDialListenerConn(l.dialer, l.name, notificationChan) + if err != nil { + return err + } + + l.lock.Lock() + defer l.lock.Unlock() + + err = l.resync(cn, notificationChan) + if err != nil { + cn.Close() + return err + } + + l.cn = cn + l.connNotificationChan = notificationChan + l.reconnectCond.Broadcast() + + return nil +} + +// Close disconnects the Listener from the database and shuts it down. +// Subsequent calls to its methods will return an error. Close returns an +// error if the connection has already been closed. +func (l *Listener) Close() error { + l.lock.Lock() + defer l.lock.Unlock() + + if l.isClosed { + return errListenerClosed + } + + if l.cn != nil { + l.cn.Close() + } + l.isClosed = true + + // Unblock calls to Listen() + l.reconnectCond.Broadcast() + + return nil +} + +func (l *Listener) emitEvent(event ListenerEventType, err error) { + if l.eventCallback != nil { + l.eventCallback(event, err) + } +} + +// Main logic here: maintain a connection to the server when possible, wait +// for notifications and emit events. +func (l *Listener) listenerConnLoop() { + var nextReconnect time.Time + + reconnectInterval := l.minReconnectInterval + for { + for { + err := l.connect() + if err == nil { + break + } + + if l.closed() { + return + } + l.emitEvent(ListenerEventConnectionAttemptFailed, err) + + time.Sleep(reconnectInterval) + reconnectInterval *= 2 + if reconnectInterval > l.maxReconnectInterval { + reconnectInterval = l.maxReconnectInterval + } + } + + if nextReconnect.IsZero() { + l.emitEvent(ListenerEventConnected, nil) + } else { + l.emitEvent(ListenerEventReconnected, nil) + l.Notify <- nil + } + + reconnectInterval = l.minReconnectInterval + nextReconnect = time.Now().Add(reconnectInterval) + + for { + notification, ok := <-l.connNotificationChan + if !ok { + // lost connection, loop again + break + } + l.Notify <- notification + } + + err := l.disconnectCleanup() + if l.closed() { + return + } + l.emitEvent(ListenerEventDisconnected, err) + + time.Sleep(time.Until(nextReconnect)) + } +} + +func (l *Listener) listenerMain() { + l.listenerConnLoop() + close(l.Notify) +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/oid/doc.go b/project/10-posts/src/vendor/github.com/lib/pq/oid/doc.go new file mode 100644 index 0000000..caaede2 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/oid/doc.go @@ -0,0 +1,6 @@ +// Package oid contains OID constants +// as defined by the Postgres server. +package oid + +// Oid is a Postgres Object ID. +type Oid uint32 diff --git a/project/10-posts/src/vendor/github.com/lib/pq/oid/types.go b/project/10-posts/src/vendor/github.com/lib/pq/oid/types.go new file mode 100644 index 0000000..ecc84c2 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/oid/types.go @@ -0,0 +1,343 @@ +// Code generated by gen.go. DO NOT EDIT. + +package oid + +const ( + T_bool Oid = 16 + T_bytea Oid = 17 + T_char Oid = 18 + T_name Oid = 19 + T_int8 Oid = 20 + T_int2 Oid = 21 + T_int2vector Oid = 22 + T_int4 Oid = 23 + T_regproc Oid = 24 + T_text Oid = 25 + T_oid Oid = 26 + T_tid Oid = 27 + T_xid Oid = 28 + T_cid Oid = 29 + T_oidvector Oid = 30 + T_pg_ddl_command Oid = 32 + T_pg_type Oid = 71 + T_pg_attribute Oid = 75 + T_pg_proc Oid = 81 + T_pg_class Oid = 83 + T_json Oid = 114 + T_xml Oid = 142 + T__xml Oid = 143 + T_pg_node_tree Oid = 194 + T__json Oid = 199 + T_smgr Oid = 210 + T_index_am_handler Oid = 325 + T_point Oid = 600 + T_lseg Oid = 601 + T_path Oid = 602 + T_box Oid = 603 + T_polygon Oid = 604 + T_line Oid = 628 + T__line Oid = 629 + T_cidr Oid = 650 + T__cidr Oid = 651 + T_float4 Oid = 700 + T_float8 Oid = 701 + T_abstime Oid = 702 + T_reltime Oid = 703 + T_tinterval Oid = 704 + T_unknown Oid = 705 + T_circle Oid = 718 + T__circle Oid = 719 + T_money Oid = 790 + T__money Oid = 791 + T_macaddr Oid = 829 + T_inet Oid = 869 + T__bool Oid = 1000 + T__bytea Oid = 1001 + T__char Oid = 1002 + T__name Oid = 1003 + T__int2 Oid = 1005 + T__int2vector Oid = 1006 + T__int4 Oid = 1007 + T__regproc Oid = 1008 + T__text Oid = 1009 + T__tid Oid = 1010 + T__xid Oid = 1011 + T__cid Oid = 1012 + T__oidvector Oid = 1013 + T__bpchar Oid = 1014 + T__varchar Oid = 1015 + T__int8 Oid = 1016 + T__point Oid = 1017 + T__lseg Oid = 1018 + T__path Oid = 1019 + T__box Oid = 1020 + T__float4 Oid = 1021 + T__float8 Oid = 1022 + T__abstime Oid = 1023 + T__reltime Oid = 1024 + T__tinterval Oid = 1025 + T__polygon Oid = 1027 + T__oid Oid = 1028 + T_aclitem Oid = 1033 + T__aclitem Oid = 1034 + T__macaddr Oid = 1040 + T__inet Oid = 1041 + T_bpchar Oid = 1042 + T_varchar Oid = 1043 + T_date Oid = 1082 + T_time Oid = 1083 + T_timestamp Oid = 1114 + T__timestamp Oid = 1115 + T__date Oid = 1182 + T__time Oid = 1183 + T_timestamptz Oid = 1184 + T__timestamptz Oid = 1185 + T_interval Oid = 1186 + T__interval Oid = 1187 + T__numeric Oid = 1231 + T_pg_database Oid = 1248 + T__cstring Oid = 1263 + T_timetz Oid = 1266 + T__timetz Oid = 1270 + T_bit Oid = 1560 + T__bit Oid = 1561 + T_varbit Oid = 1562 + T__varbit Oid = 1563 + T_numeric Oid = 1700 + T_refcursor Oid = 1790 + T__refcursor Oid = 2201 + T_regprocedure Oid = 2202 + T_regoper Oid = 2203 + T_regoperator Oid = 2204 + T_regclass Oid = 2205 + T_regtype Oid = 2206 + T__regprocedure Oid = 2207 + T__regoper Oid = 2208 + T__regoperator Oid = 2209 + T__regclass Oid = 2210 + T__regtype Oid = 2211 + T_record Oid = 2249 + T_cstring Oid = 2275 + T_any Oid = 2276 + T_anyarray Oid = 2277 + T_void Oid = 2278 + T_trigger Oid = 2279 + T_language_handler Oid = 2280 + T_internal Oid = 2281 + T_opaque Oid = 2282 + T_anyelement Oid = 2283 + T__record Oid = 2287 + T_anynonarray Oid = 2776 + T_pg_authid Oid = 2842 + T_pg_auth_members Oid = 2843 + T__txid_snapshot Oid = 2949 + T_uuid Oid = 2950 + T__uuid Oid = 2951 + T_txid_snapshot Oid = 2970 + T_fdw_handler Oid = 3115 + T_pg_lsn Oid = 3220 + T__pg_lsn Oid = 3221 + T_tsm_handler Oid = 3310 + T_anyenum Oid = 3500 + T_tsvector Oid = 3614 + T_tsquery Oid = 3615 + T_gtsvector Oid = 3642 + T__tsvector Oid = 3643 + T__gtsvector Oid = 3644 + T__tsquery Oid = 3645 + T_regconfig Oid = 3734 + T__regconfig Oid = 3735 + T_regdictionary Oid = 3769 + T__regdictionary Oid = 3770 + T_jsonb Oid = 3802 + T__jsonb Oid = 3807 + T_anyrange Oid = 3831 + T_event_trigger Oid = 3838 + T_int4range Oid = 3904 + T__int4range Oid = 3905 + T_numrange Oid = 3906 + T__numrange Oid = 3907 + T_tsrange Oid = 3908 + T__tsrange Oid = 3909 + T_tstzrange Oid = 3910 + T__tstzrange Oid = 3911 + T_daterange Oid = 3912 + T__daterange Oid = 3913 + T_int8range Oid = 3926 + T__int8range Oid = 3927 + T_pg_shseclabel Oid = 4066 + T_regnamespace Oid = 4089 + T__regnamespace Oid = 4090 + T_regrole Oid = 4096 + T__regrole Oid = 4097 +) + +var TypeName = map[Oid]string{ + T_bool: "BOOL", + T_bytea: "BYTEA", + T_char: "CHAR", + T_name: "NAME", + T_int8: "INT8", + T_int2: "INT2", + T_int2vector: "INT2VECTOR", + T_int4: "INT4", + T_regproc: "REGPROC", + T_text: "TEXT", + T_oid: "OID", + T_tid: "TID", + T_xid: "XID", + T_cid: "CID", + T_oidvector: "OIDVECTOR", + T_pg_ddl_command: "PG_DDL_COMMAND", + T_pg_type: "PG_TYPE", + T_pg_attribute: "PG_ATTRIBUTE", + T_pg_proc: "PG_PROC", + T_pg_class: "PG_CLASS", + T_json: "JSON", + T_xml: "XML", + T__xml: "_XML", + T_pg_node_tree: "PG_NODE_TREE", + T__json: "_JSON", + T_smgr: "SMGR", + T_index_am_handler: "INDEX_AM_HANDLER", + T_point: "POINT", + T_lseg: "LSEG", + T_path: "PATH", + T_box: "BOX", + T_polygon: "POLYGON", + T_line: "LINE", + T__line: "_LINE", + T_cidr: "CIDR", + T__cidr: "_CIDR", + T_float4: "FLOAT4", + T_float8: "FLOAT8", + T_abstime: "ABSTIME", + T_reltime: "RELTIME", + T_tinterval: "TINTERVAL", + T_unknown: "UNKNOWN", + T_circle: "CIRCLE", + T__circle: "_CIRCLE", + T_money: "MONEY", + T__money: "_MONEY", + T_macaddr: "MACADDR", + T_inet: "INET", + T__bool: "_BOOL", + T__bytea: "_BYTEA", + T__char: "_CHAR", + T__name: "_NAME", + T__int2: "_INT2", + T__int2vector: "_INT2VECTOR", + T__int4: "_INT4", + T__regproc: "_REGPROC", + T__text: "_TEXT", + T__tid: "_TID", + T__xid: "_XID", + T__cid: "_CID", + T__oidvector: "_OIDVECTOR", + T__bpchar: "_BPCHAR", + T__varchar: "_VARCHAR", + T__int8: "_INT8", + T__point: "_POINT", + T__lseg: "_LSEG", + T__path: "_PATH", + T__box: "_BOX", + T__float4: "_FLOAT4", + T__float8: "_FLOAT8", + T__abstime: "_ABSTIME", + T__reltime: "_RELTIME", + T__tinterval: "_TINTERVAL", + T__polygon: "_POLYGON", + T__oid: "_OID", + T_aclitem: "ACLITEM", + T__aclitem: "_ACLITEM", + T__macaddr: "_MACADDR", + T__inet: "_INET", + T_bpchar: "BPCHAR", + T_varchar: "VARCHAR", + T_date: "DATE", + T_time: "TIME", + T_timestamp: "TIMESTAMP", + T__timestamp: "_TIMESTAMP", + T__date: "_DATE", + T__time: "_TIME", + T_timestamptz: "TIMESTAMPTZ", + T__timestamptz: "_TIMESTAMPTZ", + T_interval: "INTERVAL", + T__interval: "_INTERVAL", + T__numeric: "_NUMERIC", + T_pg_database: "PG_DATABASE", + T__cstring: "_CSTRING", + T_timetz: "TIMETZ", + T__timetz: "_TIMETZ", + T_bit: "BIT", + T__bit: "_BIT", + T_varbit: "VARBIT", + T__varbit: "_VARBIT", + T_numeric: "NUMERIC", + T_refcursor: "REFCURSOR", + T__refcursor: "_REFCURSOR", + T_regprocedure: "REGPROCEDURE", + T_regoper: "REGOPER", + T_regoperator: "REGOPERATOR", + T_regclass: "REGCLASS", + T_regtype: "REGTYPE", + T__regprocedure: "_REGPROCEDURE", + T__regoper: "_REGOPER", + T__regoperator: "_REGOPERATOR", + T__regclass: "_REGCLASS", + T__regtype: "_REGTYPE", + T_record: "RECORD", + T_cstring: "CSTRING", + T_any: "ANY", + T_anyarray: "ANYARRAY", + T_void: "VOID", + T_trigger: "TRIGGER", + T_language_handler: "LANGUAGE_HANDLER", + T_internal: "INTERNAL", + T_opaque: "OPAQUE", + T_anyelement: "ANYELEMENT", + T__record: "_RECORD", + T_anynonarray: "ANYNONARRAY", + T_pg_authid: "PG_AUTHID", + T_pg_auth_members: "PG_AUTH_MEMBERS", + T__txid_snapshot: "_TXID_SNAPSHOT", + T_uuid: "UUID", + T__uuid: "_UUID", + T_txid_snapshot: "TXID_SNAPSHOT", + T_fdw_handler: "FDW_HANDLER", + T_pg_lsn: "PG_LSN", + T__pg_lsn: "_PG_LSN", + T_tsm_handler: "TSM_HANDLER", + T_anyenum: "ANYENUM", + T_tsvector: "TSVECTOR", + T_tsquery: "TSQUERY", + T_gtsvector: "GTSVECTOR", + T__tsvector: "_TSVECTOR", + T__gtsvector: "_GTSVECTOR", + T__tsquery: "_TSQUERY", + T_regconfig: "REGCONFIG", + T__regconfig: "_REGCONFIG", + T_regdictionary: "REGDICTIONARY", + T__regdictionary: "_REGDICTIONARY", + T_jsonb: "JSONB", + T__jsonb: "_JSONB", + T_anyrange: "ANYRANGE", + T_event_trigger: "EVENT_TRIGGER", + T_int4range: "INT4RANGE", + T__int4range: "_INT4RANGE", + T_numrange: "NUMRANGE", + T__numrange: "_NUMRANGE", + T_tsrange: "TSRANGE", + T__tsrange: "_TSRANGE", + T_tstzrange: "TSTZRANGE", + T__tstzrange: "_TSTZRANGE", + T_daterange: "DATERANGE", + T__daterange: "_DATERANGE", + T_int8range: "INT8RANGE", + T__int8range: "_INT8RANGE", + T_pg_shseclabel: "PG_SHSECLABEL", + T_regnamespace: "REGNAMESPACE", + T__regnamespace: "_REGNAMESPACE", + T_regrole: "REGROLE", + T__regrole: "_REGROLE", +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/rows.go b/project/10-posts/src/vendor/github.com/lib/pq/rows.go new file mode 100644 index 0000000..c6aa5b9 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/rows.go @@ -0,0 +1,93 @@ +package pq + +import ( + "math" + "reflect" + "time" + + "github.com/lib/pq/oid" +) + +const headerSize = 4 + +type fieldDesc struct { + // The object ID of the data type. + OID oid.Oid + // The data type size (see pg_type.typlen). + // Note that negative values denote variable-width types. + Len int + // The type modifier (see pg_attribute.atttypmod). + // The meaning of the modifier is type-specific. + Mod int +} + +func (fd fieldDesc) Type() reflect.Type { + switch fd.OID { + case oid.T_int8: + return reflect.TypeOf(int64(0)) + case oid.T_int4: + return reflect.TypeOf(int32(0)) + case oid.T_int2: + return reflect.TypeOf(int16(0)) + case oid.T_varchar, oid.T_text: + return reflect.TypeOf("") + case oid.T_bool: + return reflect.TypeOf(false) + case oid.T_date, oid.T_time, oid.T_timetz, oid.T_timestamp, oid.T_timestamptz: + return reflect.TypeOf(time.Time{}) + case oid.T_bytea: + return reflect.TypeOf([]byte(nil)) + default: + return reflect.TypeOf(new(interface{})).Elem() + } +} + +func (fd fieldDesc) Name() string { + return oid.TypeName[fd.OID] +} + +func (fd fieldDesc) Length() (length int64, ok bool) { + switch fd.OID { + case oid.T_text, oid.T_bytea: + return math.MaxInt64, true + case oid.T_varchar, oid.T_bpchar: + return int64(fd.Mod - headerSize), true + default: + return 0, false + } +} + +func (fd fieldDesc) PrecisionScale() (precision, scale int64, ok bool) { + switch fd.OID { + case oid.T_numeric, oid.T__numeric: + mod := fd.Mod - headerSize + precision = int64((mod >> 16) & 0xffff) + scale = int64(mod & 0xffff) + return precision, scale, true + default: + return 0, 0, false + } +} + +// ColumnTypeScanType returns the value type that can be used to scan types into. +func (rs *rows) ColumnTypeScanType(index int) reflect.Type { + return rs.colTyps[index].Type() +} + +// ColumnTypeDatabaseTypeName return the database system type name. +func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { + return rs.colTyps[index].Name() +} + +// ColumnTypeLength returns the length of the column type if the column is a +// variable length type. If the column is not a variable length type ok +// should return false. +func (rs *rows) ColumnTypeLength(index int) (length int64, ok bool) { + return rs.colTyps[index].Length() +} + +// ColumnTypePrecisionScale should return the precision and scale for decimal +// types. If not applicable, ok should be false. +func (rs *rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + return rs.colTyps[index].PrecisionScale() +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/scram/scram.go b/project/10-posts/src/vendor/github.com/lib/pq/scram/scram.go new file mode 100644 index 0000000..477216b --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/scram/scram.go @@ -0,0 +1,264 @@ +// Copyright (c) 2014 - Gustavo Niemeyer +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package scram implements a SCRAM-{SHA-1,etc} client per RFC5802. +// +// http://tools.ietf.org/html/rfc5802 +// +package scram + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "fmt" + "hash" + "strconv" + "strings" +) + +// Client implements a SCRAM-* client (SCRAM-SHA-1, SCRAM-SHA-256, etc). +// +// A Client may be used within a SASL conversation with logic resembling: +// +// var in []byte +// var client = scram.NewClient(sha1.New, user, pass) +// for client.Step(in) { +// out := client.Out() +// // send out to server +// in := serverOut +// } +// if client.Err() != nil { +// // auth failed +// } +// +type Client struct { + newHash func() hash.Hash + + user string + pass string + step int + out bytes.Buffer + err error + + clientNonce []byte + serverNonce []byte + saltedPass []byte + authMsg bytes.Buffer +} + +// NewClient returns a new SCRAM-* client with the provided hash algorithm. +// +// For SCRAM-SHA-256, for example, use: +// +// client := scram.NewClient(sha256.New, user, pass) +// +func NewClient(newHash func() hash.Hash, user, pass string) *Client { + c := &Client{ + newHash: newHash, + user: user, + pass: pass, + } + c.out.Grow(256) + c.authMsg.Grow(256) + return c +} + +// Out returns the data to be sent to the server in the current step. +func (c *Client) Out() []byte { + if c.out.Len() == 0 { + return nil + } + return c.out.Bytes() +} + +// Err returns the error that occurred, or nil if there were no errors. +func (c *Client) Err() error { + return c.err +} + +// SetNonce sets the client nonce to the provided value. +// If not set, the nonce is generated automatically out of crypto/rand on the first step. +func (c *Client) SetNonce(nonce []byte) { + c.clientNonce = nonce +} + +var escaper = strings.NewReplacer("=", "=3D", ",", "=2C") + +// Step processes the incoming data from the server and makes the +// next round of data for the server available via Client.Out. +// Step returns false if there are no errors and more data is +// still expected. +func (c *Client) Step(in []byte) bool { + c.out.Reset() + if c.step > 2 || c.err != nil { + return false + } + c.step++ + switch c.step { + case 1: + c.err = c.step1(in) + case 2: + c.err = c.step2(in) + case 3: + c.err = c.step3(in) + } + return c.step > 2 || c.err != nil +} + +func (c *Client) step1(in []byte) error { + if len(c.clientNonce) == 0 { + const nonceLen = 16 + buf := make([]byte, nonceLen+b64.EncodedLen(nonceLen)) + if _, err := rand.Read(buf[:nonceLen]); err != nil { + return fmt.Errorf("cannot read random SCRAM-SHA-256 nonce from operating system: %v", err) + } + c.clientNonce = buf[nonceLen:] + b64.Encode(c.clientNonce, buf[:nonceLen]) + } + c.authMsg.WriteString("n=") + escaper.WriteString(&c.authMsg, c.user) + c.authMsg.WriteString(",r=") + c.authMsg.Write(c.clientNonce) + + c.out.WriteString("n,,") + c.out.Write(c.authMsg.Bytes()) + return nil +} + +var b64 = base64.StdEncoding + +func (c *Client) step2(in []byte) error { + c.authMsg.WriteByte(',') + c.authMsg.Write(in) + + fields := bytes.Split(in, []byte(",")) + if len(fields) != 3 { + return fmt.Errorf("expected 3 fields in first SCRAM-SHA-256 server message, got %d: %q", len(fields), in) + } + if !bytes.HasPrefix(fields[0], []byte("r=")) || len(fields[0]) < 2 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 nonce: %q", fields[0]) + } + if !bytes.HasPrefix(fields[1], []byte("s=")) || len(fields[1]) < 6 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 salt: %q", fields[1]) + } + if !bytes.HasPrefix(fields[2], []byte("i=")) || len(fields[2]) < 6 { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) + } + + c.serverNonce = fields[0][2:] + if !bytes.HasPrefix(c.serverNonce, c.clientNonce) { + return fmt.Errorf("server SCRAM-SHA-256 nonce is not prefixed by client nonce: got %q, want %q+\"...\"", c.serverNonce, c.clientNonce) + } + + salt := make([]byte, b64.DecodedLen(len(fields[1][2:]))) + n, err := b64.Decode(salt, fields[1][2:]) + if err != nil { + return fmt.Errorf("cannot decode SCRAM-SHA-256 salt sent by server: %q", fields[1]) + } + salt = salt[:n] + iterCount, err := strconv.Atoi(string(fields[2][2:])) + if err != nil { + return fmt.Errorf("server sent an invalid SCRAM-SHA-256 iteration count: %q", fields[2]) + } + c.saltPassword(salt, iterCount) + + c.authMsg.WriteString(",c=biws,r=") + c.authMsg.Write(c.serverNonce) + + c.out.WriteString("c=biws,r=") + c.out.Write(c.serverNonce) + c.out.WriteString(",p=") + c.out.Write(c.clientProof()) + return nil +} + +func (c *Client) step3(in []byte) error { + var isv, ise bool + var fields = bytes.Split(in, []byte(",")) + if len(fields) == 1 { + isv = bytes.HasPrefix(fields[0], []byte("v=")) + ise = bytes.HasPrefix(fields[0], []byte("e=")) + } + if ise { + return fmt.Errorf("SCRAM-SHA-256 authentication error: %s", fields[0][2:]) + } else if !isv { + return fmt.Errorf("unsupported SCRAM-SHA-256 final message from server: %q", in) + } + if !bytes.Equal(c.serverSignature(), fields[0][2:]) { + return fmt.Errorf("cannot authenticate SCRAM-SHA-256 server signature: %q", fields[0][2:]) + } + return nil +} + +func (c *Client) saltPassword(salt []byte, iterCount int) { + mac := hmac.New(c.newHash, []byte(c.pass)) + mac.Write(salt) + mac.Write([]byte{0, 0, 0, 1}) + ui := mac.Sum(nil) + hi := make([]byte, len(ui)) + copy(hi, ui) + for i := 1; i < iterCount; i++ { + mac.Reset() + mac.Write(ui) + mac.Sum(ui[:0]) + for j, b := range ui { + hi[j] ^= b + } + } + c.saltedPass = hi +} + +func (c *Client) clientProof() []byte { + mac := hmac.New(c.newHash, c.saltedPass) + mac.Write([]byte("Client Key")) + clientKey := mac.Sum(nil) + hash := c.newHash() + hash.Write(clientKey) + storedKey := hash.Sum(nil) + mac = hmac.New(c.newHash, storedKey) + mac.Write(c.authMsg.Bytes()) + clientProof := mac.Sum(nil) + for i, b := range clientKey { + clientProof[i] ^= b + } + clientProof64 := make([]byte, b64.EncodedLen(len(clientProof))) + b64.Encode(clientProof64, clientProof) + return clientProof64 +} + +func (c *Client) serverSignature() []byte { + mac := hmac.New(c.newHash, c.saltedPass) + mac.Write([]byte("Server Key")) + serverKey := mac.Sum(nil) + + mac = hmac.New(c.newHash, serverKey) + mac.Write(c.authMsg.Bytes()) + serverSignature := mac.Sum(nil) + + encoded := make([]byte, b64.EncodedLen(len(serverSignature))) + b64.Encode(encoded, serverSignature) + return encoded +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/ssl.go b/project/10-posts/src/vendor/github.com/lib/pq/ssl.go new file mode 100644 index 0000000..36b61ba --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/ssl.go @@ -0,0 +1,204 @@ +package pq + +import ( + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "os" + "os/user" + "path/filepath" + "strings" +) + +// ssl generates a function to upgrade a net.Conn based on the "sslmode" and +// related settings. The function is nil when no upgrade should take place. +func ssl(o values) (func(net.Conn) (net.Conn, error), error) { + verifyCaOnly := false + tlsConf := tls.Config{} + switch mode := o["sslmode"]; mode { + // "require" is the default. + case "", "require": + // We must skip TLS's own verification since it requires full + // verification since Go 1.3. + tlsConf.InsecureSkipVerify = true + + // From http://www.postgresql.org/docs/current/static/libpq-ssl.html: + // + // Note: For backwards compatibility with earlier versions of + // PostgreSQL, if a root CA file exists, the behavior of + // sslmode=require will be the same as that of verify-ca, meaning the + // server certificate is validated against the CA. Relying on this + // behavior is discouraged, and applications that need certificate + // validation should always use verify-ca or verify-full. + if sslrootcert, ok := o["sslrootcert"]; ok { + if _, err := os.Stat(sslrootcert); err == nil { + verifyCaOnly = true + } else { + delete(o, "sslrootcert") + } + } + case "verify-ca": + // We must skip TLS's own verification since it requires full + // verification since Go 1.3. + tlsConf.InsecureSkipVerify = true + verifyCaOnly = true + case "verify-full": + tlsConf.ServerName = o["host"] + case "disable": + return nil, nil + default: + return nil, fmterrorf(`unsupported sslmode %q; only "require" (default), "verify-full", "verify-ca", and "disable" supported`, mode) + } + + // Set Server Name Indication (SNI), if enabled by connection parameters. + // By default SNI is on, any value which is not starting with "1" disables + // SNI -- that is the same check vanilla libpq uses. + if sslsni := o["sslsni"]; sslsni == "" || strings.HasPrefix(sslsni, "1") { + // RFC 6066 asks to not set SNI if the host is a literal IP address (IPv4 + // or IPv6). This check is coded already crypto.tls.hostnameInSNI, so + // just always set ServerName here and let crypto/tls do the filtering. + tlsConf.ServerName = o["host"] + } + + err := sslClientCertificates(&tlsConf, o) + if err != nil { + return nil, err + } + err = sslCertificateAuthority(&tlsConf, o) + if err != nil { + return nil, err + } + + // Accept renegotiation requests initiated by the backend. + // + // Renegotiation was deprecated then removed from PostgreSQL 9.5, but + // the default configuration of older versions has it enabled. Redshift + // also initiates renegotiations and cannot be reconfigured. + tlsConf.Renegotiation = tls.RenegotiateFreelyAsClient + + return func(conn net.Conn) (net.Conn, error) { + client := tls.Client(conn, &tlsConf) + if verifyCaOnly { + err := sslVerifyCertificateAuthority(client, &tlsConf) + if err != nil { + return nil, err + } + } + return client, nil + }, nil +} + +// sslClientCertificates adds the certificate specified in the "sslcert" and +// "sslkey" settings, or if they aren't set, from the .postgresql directory +// in the user's home directory. The configured files must exist and have +// the correct permissions. +func sslClientCertificates(tlsConf *tls.Config, o values) error { + sslinline := o["sslinline"] + if sslinline == "true" { + cert, err := tls.X509KeyPair([]byte(o["sslcert"]), []byte(o["sslkey"])) + if err != nil { + return err + } + tlsConf.Certificates = []tls.Certificate{cert} + return nil + } + + // user.Current() might fail when cross-compiling. We have to ignore the + // error and continue without home directory defaults, since we wouldn't + // know from where to load them. + user, _ := user.Current() + + // In libpq, the client certificate is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1036-L1037 + sslcert := o["sslcert"] + if len(sslcert) == 0 && user != nil { + sslcert = filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt") + } + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1045 + if len(sslcert) == 0 { + return nil + } + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1050:L1054 + if _, err := os.Stat(sslcert); os.IsNotExist(err) { + return nil + } else if err != nil { + return err + } + + // In libpq, the ssl key is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L1123-L1222 + sslkey := o["sslkey"] + if len(sslkey) == 0 && user != nil { + sslkey = filepath.Join(user.HomeDir, ".postgresql", "postgresql.key") + } + + if len(sslkey) > 0 { + if err := sslKeyPermissions(sslkey); err != nil { + return err + } + } + + cert, err := tls.LoadX509KeyPair(sslcert, sslkey) + if err != nil { + return err + } + + tlsConf.Certificates = []tls.Certificate{cert} + return nil +} + +// sslCertificateAuthority adds the RootCA specified in the "sslrootcert" setting. +func sslCertificateAuthority(tlsConf *tls.Config, o values) error { + // In libpq, the root certificate is only loaded if the setting is not blank. + // + // https://github.com/postgres/postgres/blob/REL9_6_2/src/interfaces/libpq/fe-secure-openssl.c#L950-L951 + if sslrootcert := o["sslrootcert"]; len(sslrootcert) > 0 { + tlsConf.RootCAs = x509.NewCertPool() + + sslinline := o["sslinline"] + + var cert []byte + if sslinline == "true" { + cert = []byte(sslrootcert) + } else { + var err error + cert, err = ioutil.ReadFile(sslrootcert) + if err != nil { + return err + } + } + + if !tlsConf.RootCAs.AppendCertsFromPEM(cert) { + return fmterrorf("couldn't parse pem in sslrootcert") + } + } + + return nil +} + +// sslVerifyCertificateAuthority carries out a TLS handshake to the server and +// verifies the presented certificate against the CA, i.e. the one specified in +// sslrootcert or the system CA if sslrootcert was not specified. +func sslVerifyCertificateAuthority(client *tls.Conn, tlsConf *tls.Config) error { + err := client.Handshake() + if err != nil { + return err + } + certs := client.ConnectionState().PeerCertificates + opts := x509.VerifyOptions{ + DNSName: client.ConnectionState().ServerName, + Intermediates: x509.NewCertPool(), + Roots: tlsConf.RootCAs, + } + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + _, err = certs[0].Verify(opts) + return err +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/ssl_permissions.go b/project/10-posts/src/vendor/github.com/lib/pq/ssl_permissions.go new file mode 100644 index 0000000..d587f10 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/ssl_permissions.go @@ -0,0 +1,93 @@ +//go:build !windows +// +build !windows + +package pq + +import ( + "errors" + "os" + "syscall" +) + +const ( + rootUserID = uint32(0) + + // The maximum permissions that a private key file owned by a regular user + // is allowed to have. This translates to u=rw. + maxUserOwnedKeyPermissions os.FileMode = 0600 + + // The maximum permissions that a private key file owned by root is allowed + // to have. This translates to u=rw,g=r. + maxRootOwnedKeyPermissions os.FileMode = 0640 +) + +var ( + errSSLKeyHasUnacceptableUserPermissions = errors.New("permissions for files not owned by root should be u=rw (0600) or less") + errSSLKeyHasUnacceptableRootPermissions = errors.New("permissions for root owned files should be u=rw,g=r (0640) or less") +) + +// sslKeyPermissions checks the permissions on user-supplied ssl key files. +// The key file should have very little access. +// +// libpq does not check key file permissions on Windows. +func sslKeyPermissions(sslkey string) error { + info, err := os.Stat(sslkey) + if err != nil { + return err + } + + err = hasCorrectPermissions(info) + + // return ErrSSLKeyHasWorldPermissions for backwards compatability with + // existing code. + if err == errSSLKeyHasUnacceptableUserPermissions || err == errSSLKeyHasUnacceptableRootPermissions { + err = ErrSSLKeyHasWorldPermissions + } + return err +} + +// hasCorrectPermissions checks the file info (and the unix-specific stat_t +// output) to verify that the permissions on the file are correct. +// +// If the file is owned by the same user the process is running as, +// the file should only have 0600 (u=rw). If the file is owned by root, +// and the group matches the group that the process is running in, the +// permissions cannot be more than 0640 (u=rw,g=r). The file should +// never have world permissions. +// +// Returns an error when the permission check fails. +func hasCorrectPermissions(info os.FileInfo) error { + // if file's permission matches 0600, allow access. + userPermissionMask := (os.FileMode(0777) ^ maxUserOwnedKeyPermissions) + + // regardless of if we're running as root or not, 0600 is acceptable, + // so we return if we match the regular user permission mask. + if info.Mode().Perm()&userPermissionMask == 0 { + return nil + } + + // We need to pull the Unix file information to get the file's owner. + // If we can't access it, there's some sort of operating system level error + // and we should fail rather than attempting to use faulty information. + sysInfo := info.Sys() + if sysInfo == nil { + return ErrSSLKeyUnknownOwnership + } + + unixStat, ok := sysInfo.(*syscall.Stat_t) + if !ok { + return ErrSSLKeyUnknownOwnership + } + + // if the file is owned by root, we allow 0640 (u=rw,g=r) to match what + // Postgres does. + if unixStat.Uid == rootUserID { + rootPermissionMask := (os.FileMode(0777) ^ maxRootOwnedKeyPermissions) + if info.Mode().Perm()&rootPermissionMask != 0 { + return errSSLKeyHasUnacceptableRootPermissions + } + return nil + } + + return errSSLKeyHasUnacceptableUserPermissions +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/ssl_windows.go b/project/10-posts/src/vendor/github.com/lib/pq/ssl_windows.go new file mode 100644 index 0000000..73663c8 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/ssl_windows.go @@ -0,0 +1,10 @@ +//go:build windows +// +build windows + +package pq + +// sslKeyPermissions checks the permissions on user-supplied ssl key files. +// The key file should have very little access. +// +// libpq does not check key file permissions on Windows. +func sslKeyPermissions(string) error { return nil } diff --git a/project/10-posts/src/vendor/github.com/lib/pq/url.go b/project/10-posts/src/vendor/github.com/lib/pq/url.go new file mode 100644 index 0000000..aec6e95 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/url.go @@ -0,0 +1,76 @@ +package pq + +import ( + "fmt" + "net" + nurl "net/url" + "sort" + "strings" +) + +// ParseURL no longer needs to be used by clients of this library since supplying a URL as a +// connection string to sql.Open() is now supported: +// +// sql.Open("postgres", "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full") +// +// It remains exported here for backwards-compatibility. +// +// ParseURL converts a url to a connection string for driver.Open. +// Example: +// +// "postgres://bob:secret@1.2.3.4:5432/mydb?sslmode=verify-full" +// +// converts to: +// +// "user=bob password=secret host=1.2.3.4 port=5432 dbname=mydb sslmode=verify-full" +// +// A minimal example: +// +// "postgres://" +// +// This will be blank, causing driver.Open to use all of the defaults +func ParseURL(url string) (string, error) { + u, err := nurl.Parse(url) + if err != nil { + return "", err + } + + if u.Scheme != "postgres" && u.Scheme != "postgresql" { + return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) + } + + var kvs []string + escaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`) + accrue := func(k, v string) { + if v != "" { + kvs = append(kvs, k+"='"+escaper.Replace(v)+"'") + } + } + + if u.User != nil { + v := u.User.Username() + accrue("user", v) + + v, _ = u.User.Password() + accrue("password", v) + } + + if host, port, err := net.SplitHostPort(u.Host); err != nil { + accrue("host", u.Host) + } else { + accrue("host", host) + accrue("port", port) + } + + if u.Path != "" { + accrue("dbname", u.Path[1:]) + } + + q := u.Query() + for k := range q { + accrue(k, q.Get(k)) + } + + sort.Strings(kvs) // Makes testing easier (not a performance concern) + return strings.Join(kvs, " "), nil +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/user_other.go b/project/10-posts/src/vendor/github.com/lib/pq/user_other.go new file mode 100644 index 0000000..3dae8f5 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/user_other.go @@ -0,0 +1,10 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. + +//go:build js || android || hurd || zos +// +build js android hurd zos + +package pq + +func userCurrent() (string, error) { + return "", ErrCouldNotDetectUsername +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/user_posix.go b/project/10-posts/src/vendor/github.com/lib/pq/user_posix.go new file mode 100644 index 0000000..5f2d439 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/user_posix.go @@ -0,0 +1,25 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. + +//go:build aix || darwin || dragonfly || freebsd || (linux && !android) || nacl || netbsd || openbsd || plan9 || solaris || rumprun || illumos +// +build aix darwin dragonfly freebsd linux,!android nacl netbsd openbsd plan9 solaris rumprun illumos + +package pq + +import ( + "os" + "os/user" +) + +func userCurrent() (string, error) { + u, err := user.Current() + if err == nil { + return u.Username, nil + } + + name := os.Getenv("USER") + if name != "" { + return name, nil + } + + return "", ErrCouldNotDetectUsername +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/user_windows.go b/project/10-posts/src/vendor/github.com/lib/pq/user_windows.go new file mode 100644 index 0000000..2b69126 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/user_windows.go @@ -0,0 +1,27 @@ +// Package pq is a pure Go Postgres driver for the database/sql package. +package pq + +import ( + "path/filepath" + "syscall" +) + +// Perform Windows user name lookup identically to libpq. +// +// The PostgreSQL code makes use of the legacy Win32 function +// GetUserName, and that function has not been imported into stock Go. +// GetUserNameEx is available though, the difference being that a +// wider range of names are available. To get the output to be the +// same as GetUserName, only the base (or last) component of the +// result is returned. +func userCurrent() (string, error) { + pw_name := make([]uint16, 128) + pwname_size := uint32(len(pw_name)) - 1 + err := syscall.GetUserNameEx(syscall.NameSamCompatible, &pw_name[0], &pwname_size) + if err != nil { + return "", ErrCouldNotDetectUsername + } + s := syscall.UTF16ToString(pw_name) + u := filepath.Base(s) + return u, nil +} diff --git a/project/10-posts/src/vendor/github.com/lib/pq/uuid.go b/project/10-posts/src/vendor/github.com/lib/pq/uuid.go new file mode 100644 index 0000000..9a1b9e0 --- /dev/null +++ b/project/10-posts/src/vendor/github.com/lib/pq/uuid.go @@ -0,0 +1,23 @@ +package pq + +import ( + "encoding/hex" + "fmt" +) + +// decodeUUIDBinary interprets the binary format of a uuid, returning it in text format. +func decodeUUIDBinary(src []byte) ([]byte, error) { + if len(src) != 16 { + return nil, fmt.Errorf("pq: unable to decode uuid; bad length: %d", len(src)) + } + + dst := make([]byte, 36) + dst[8], dst[13], dst[18], dst[23] = '-', '-', '-', '-' + hex.Encode(dst[0:], src[0:4]) + hex.Encode(dst[9:], src[4:6]) + hex.Encode(dst[14:], src[6:8]) + hex.Encode(dst[19:], src[8:10]) + hex.Encode(dst[24:], src[10:16]) + + return dst, nil +} diff --git a/project/10-posts/src/vendor/modules.txt b/project/10-posts/src/vendor/modules.txt new file mode 100644 index 0000000..5a1c6aa --- /dev/null +++ b/project/10-posts/src/vendor/modules.txt @@ -0,0 +1,17 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/google/uuid v1.3.0 +## explicit +github.com/google/uuid +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv +# github.com/lib/pq v1.10.7 +## explicit; go 1.13 +github.com/lib/pq +github.com/lib/pq/oid +github.com/lib/pq/scram diff --git a/project/11-submit/readme.md b/project/11-submit/readme.md new file mode 100644 index 0000000..51320cf --- /dev/null +++ b/project/11-submit/readme.md @@ -0,0 +1,17 @@ +# Submit the link to your Git repository! + +Your link should look something like `https://github.com/gihub-username/repo-name`. + +## Ideas for extending the project + +You don't *have* to extend this project, but here are just a few ideas if you're interested: + +* Support [pagination](https://nordicapis.com/everything-you-need-to-know-about-api-pagination/) of the endpoints that can return many items +* Support different options for sorting and filtering posts using query parameters +* Classify different types of feeds and posts (e.g. blog, podcast, video, etc.) +* Add a CLI client that uses the API to fetch and display posts, maybe it even allows you to read them in your terminal +* Scrape lists of feeds themselves from a third-party site that aggregates feed URLs +* Add support for other types of feeds (e.g. Atom, JSON, etc.) +* Add integration tests that use the API to create, read, update, and delete feeds and posts +* Add bookmarking or "liking" to posts +* Create a simple web UI that uses your backend API diff --git a/project/2-boilerplate/readme.md b/project/2-boilerplate/readme.md new file mode 100644 index 0000000..968a95c --- /dev/null +++ b/project/2-boilerplate/readme.md @@ -0,0 +1,88 @@ +# Boilerplate + +Before we get to the app-specific stuff, let's scaffold a simple CRUD server, hopefully, you're already familiar with how to do this from the "Learn Web Servers" course! That said, I'll provide a quick refresher. + +*It might be a good idea to use your "Learn Web Servers" code as a reference while building this project!* + +## 1. Create a new project + +You should know how to do this by now! My process is: + +* Create a repo on GitHub or GitLab (initialized with a README) +* Clone it onto your machine +* Create a new Go module with `go mod init`. +* Create a `main.go` file in the root of your project, and add a `func main()` to it. + +## 2. Install packages + +Install the following packages using `go get`: + +* [chi](https://github.com/go-chi/chi) +* [cors]https://github.com/go-chi/cors +* [godotenv](github.com/joho/godotenv) + +## 3. Env + +Create a gitignore'd `.env` file in the root of your project and add the following: + +```bash +PORT="8080" +``` + +The `.env` file is a convenient way to store environment (configuration) variables. + +* Use [godotenv.Load()](https://pkg.go.dev/github.com/joho/godotenv#Load) to load the variables from the file into your environment at the top of `main()`. +* Use [os.Getenv()](https://pkg.go.dev/os#Getenv) to get the value of `PORT`. + +## 4. Create a router and server + +1. Create a [chi.NewRouter](https://pkg.go.dev/github.com/go-chi/chi#NewRouter) +2. Use [router.Use](https://pkg.go.dev/github.com/go-chi/chi#Router.Use) to add the built-in [cors.Handler](https://pkg.go.dev/github.com/go-chi/cors#Handler) middleware. +3. Create sub-router for the `/v1` namespace and mount it to the main router. +4. Create a new [http.Server](https://pkg.go.dev/net/http#Server) and add the port and the main router to it. +5. Start the server + +## 5. Create some JSON helper functions + +Create two functions: + +* `respondWithJSON(w http.ResponseWriter, status int, payload interface{})` +* `respondWithError(w http.ResponseWriter, code int, msg string)` (which calls `respondWithJSON` with error-specific values) + +You used these in the "Learn Web Servers" course, so you should be able to figure out how to implement them again. They're simply helper functions that write an HTTP response with: + +* A status code +* An `application/json` content type +* A JSON body + +## 6. Add a readiness handler + +Add a handler for `GET /v1/readiness` requests. It should return a 200 status code and a JSON body: + +```json +{ + "status": "ok" +} +``` + +*The purpose of this endpoint is for you to test your `respondWithJSON` function.* + +## 7. Add an error handler + +Add a handler for `GET /v1/err` requests. It should return a 500 status code and a JSON body: + +```json +{ + "error": "Internal Server Error" +} +``` + +*The purpose of this endpoint is for you to test your `respondWithError` function.* + +## 8. Run and test your server + +```bash +go build -o out && ./out +``` + +Once it's running, use an HTTP client to test your endpoints. diff --git a/project/2-boilerplate/src/.gitignore b/project/2-boilerplate/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/2-boilerplate/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/2-boilerplate/src/go.mod b/project/2-boilerplate/src/go.mod new file mode 100644 index 0000000..4e4c350 --- /dev/null +++ b/project/2-boilerplate/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/boilerplate + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/2-boilerplate/src/go.sum b/project/2-boilerplate/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/2-boilerplate/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/2-boilerplate/src/handler_ready.go b/project/2-boilerplate/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/2-boilerplate/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/2-boilerplate/src/json.go b/project/2-boilerplate/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/2-boilerplate/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/2-boilerplate/src/main.go b/project/2-boilerplate/src/main.go new file mode 100644 index 0000000..df29c39 --- /dev/null +++ b/project/2-boilerplate/src/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" +) + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/.gitignore b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/LICENSE b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/Makefile b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/README.md b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chain.go b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chi.go b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/context.go b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/mux.go b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/chi/tree.go b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/cors/LICENSE b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/cors/README.md b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/cors/cors.go b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/2-boilerplate/src/vendor/github.com/go-chi/cors/utils.go b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/2-boilerplate/src/vendor/github.com/joho/godotenv/.gitignore b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/2-boilerplate/src/vendor/github.com/joho/godotenv/LICENCE b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/2-boilerplate/src/vendor/github.com/joho/godotenv/README.md b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/2-boilerplate/src/vendor/github.com/joho/godotenv/godotenv.go b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/2-boilerplate/src/vendor/github.com/joho/godotenv/parser.go b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/2-boilerplate/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/2-boilerplate/src/vendor/modules.txt b/project/2-boilerplate/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/2-boilerplate/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/3-postgres/readme.md b/project/3-postgres/readme.md new file mode 100644 index 0000000..cfdf726 --- /dev/null +++ b/project/3-postgres/readme.md @@ -0,0 +1,95 @@ +# PostgreSQL + +PostgreSQL is a production-ready, open-source database. It's a great choice database for many web applications, and as a back-end engineer, it might be the single most important database to be familiar with. + +## How does PostgreSQL work? + +Postgres, like most other database technologies, is itself a server. It listens for requests on a port (Postgres' default is `:5432`), and responds to those requests. To interact with Postgres, first you will install the server and start it. Then, you can connect to it using a client like [psql](https://www.postgresql.org/docs/current/app-psql.html#:~:text=psql%20is%20a%20terminal%2Dbased,or%20from%20command%20line%20arguments.) or [PGAdmin](https://www.pgadmin.org/). + +## 1. Install + +### Mac OS + +I recommend using [brew](https://brew.sh/) to install PostgreSQL on Mac OS. + +```bash +brew install postgresql +``` + +### Linux (or WSL) + +I recommend using apt to install PostgreSQL on Linux (Ubuntu). Here are the [docs from Microsoft](https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-database#install-postgresql). The basic steps are: + +```bash +sudo apt update +sudo apt install postgresql postgresql-contrib +``` + +## 2. Ensure the installation worked + +The `psql` command-line utility is the default client for Postgres. Use it to make sure you're on version 14+ of Postgres: + +```bash +psql --version +``` + +## 3. Start the Postgres server in the background + +### Mac OS + +```bash +brew services start postgresql +``` + +### Linux (or WSL) + +```bash +sudo service postgresql start +``` + +## 4. Connect to the server using a client + +I'm going to recommend using the PGAdmin client. It's a GUI that makes it easy to interact with Postgres and provides a lot of useful features. If you want to use `psql` on the command line instead, you're welcome to do so. + +1. Download [PGAdmin here](https://www.pgadmin.org/). +2. Open PGAdmin and create a new server connection. Here are the connection details you'll need: + +### Mac OS (with brew) + +* Host: `localhost` +* Port: `5432` +* Username: Your Mac OS username +* Password: *leave this blank* + +### Linux (or WSL) + +If you're on Linux, you have one more step before you can connect with credentials. On your command line run: + +``` +sudo passwd postgres +``` + +Enter a new password *and remember it*. Then restart your shell session. + +* Host: `localhost` +* Port: `5432` +* Username: `postgres` +* Password: *the password you created* + +## 5. Create a database + +A single Postgres server can host multiple databases. In the dropdown menu on the left, open the `Localhost` tab, then right click on "databases" and select "create database". + +Name it whatever you like, but you'll need to know the name. + +## 6. Query the database + +Right click on your database's name in the menu on the left, then select "query tool". You should see a new window open with a text editor. In the text editor, type the following query: + +```sql +SELECT version(); +``` + +And click the triangle icon (execute/refresh) to run it. If you see a version number, you're good to go! + +*PGAdmin is the "thunder client" of Postgres. It's just a GUI that allows you to run ad-hoc queries against your database.* diff --git a/project/4-create_users/readme.md b/project/4-create_users/readme.md new file mode 100644 index 0000000..f91a758 --- /dev/null +++ b/project/4-create_users/readme.md @@ -0,0 +1,183 @@ +# Create Users + +In this step, we'll be adding an endpoint to create new users on the server. We'll be using a couple of tools to help us out: + +* [database/sql](https://pkg.go.dev/database/sql): This is part of Go's standard library. It provides a way to connect to a SQL database, execute queries, and scan the results into Go types. +* [sqlc](https://sqlc.dev/): SQLC is an *amazing* Go program that generates Go code from SQL queries. It's not exactly an [ORM](https://www.freecodecamp.org/news/what-is-an-orm-the-meaning-of-object-relational-mapping-database-tools/), but rather a tool that makes working with raw SQL almost as easy as using an ORM. +* [Goose](https://github.com/pressly/goose): Goose is a database migration tool written in Go. It runs migrations from the same SQL files that SQLC uses, making the pair of tools a perfect fit. + +## 1. Install SQLC + +SQLC is just a command line tool, it's not a package that we need to import. I recommend [installing](https://docs.sqlc.dev/en/latest/overview/install.html) it using `go install`: + +```bash +go install github.com/kyleconroy/sqlc/cmd/sqlc@latest +``` + +Then run `sqlc version` to make sure it's installed correctly. + +## 2. Install Goose + +Like SQLC, Goose is just a command line tool. I also recommend [installing](https://github.com/pressly/goose#install) it using `go install`: + +```bash +go install github.com/pressly/goose/v3/cmd/goose@latest +``` + +Run `goose -version` to make sure it's installed correctly. + +## 3. Create the `users` migration + +I recommend creating an `sql` directory in the root of your project, and in there creating a `schema` directory. + +A "migration" is a SQL file that describes a change to your database schema. For now, we need our first migration to create a `users` table. The simplest format for these files is: + +``` +number_name.sql +``` + +For example, I created a file in `sql/schema` called `001_users.sql` with the following contents: + +```sql +-- +goose Up +CREATE TABLE ... + +-- +goose Down +DROP TABLE users; +``` + +Write out the `CREATE TABLE` statement in full, I left it blank for you to fill in. A `user` should have 4 fields: + +* id: a `UUID` that will serve as the primary key +* created_at: a `TIMESTAMP` that can not be null +* updated_at: a `TIMESTAMP` that can not be null +* name: a string that can not be null + +The `-- +goose Up` and `-- +goose Down` comments are required. They tell Goose how to run the migration. An "up" migration moves your database from its old state to a new state. A "down" migration moves your database from its new state back to its old state. + +By running all of the "up" migrations on a blank database, you should end up with a database in a ready-to-use state. "Down" migrations are only used when you need to roll back a migration, or if you need to reset a local testing database to a known state. + +## 4. Run the migration + +`cd` into the `sql/schema` directory and run: + +```bash +goose postgres CONN up +``` + +Where `CONN` is the connection string for your database. Here is mine: + +``` +postgres://wagslane:@localhost:5432/blogator +``` + +The format is: + +``` +protocol://username:password@host:port/database +``` + +Run your migration! Make sure it works by using PGAdmin to find your newly created `users` table. + +## 5. Save your connection string as an environment variable + +Add your connection string to your `.env` file. When using it with `goose`, you'll use it in the format we just used. However, here in the `.env` file it needs an additional query string: + +``` +protocol://username:password@host:port/database?sslmode=disable +``` + +Your application code needs to know to not try to use SSL locally. + +## 6. Configure [SQLC](https://docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html) + +You'll always run the `sqlc` command from the root of your project. Create a file called `sqlc.yaml` in the root of your project. Here is mine: + +```yaml +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" +``` + +We're telling SQLC to look in the `sql/schema` directory for our schema structure (which is the same set of files that Goose uses, but sqlc automatically ignores "down" migrations), and in the `sql/queries` directory for queries. We're also telling it to generate Go code in the `internal/database` directory. + +## 7. Write a query to create a user + +Inside the `sql/queries` directory, create a file called `users.sql`. Here is mine: + +```sql +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name) +VALUES ($1, $2, $3, $4) +RETURNING *; +``` + +`$1`, `$2`, `$3`, and `$4` are parameters that we'll be able to pass into the query in our Go code. The `:one` at the end of the query name tells SQLC that we expect to get back a single row (the created user). + +Keep the [SQLC docs](https://docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html) handy, you'll probably need to refer to them again later. + +## 8. Generate the Go code + +Run `sqlc generate` from the root of your project. It should create a new package of go code in `internal/database`. + +## 9. Open a connection to the database, and store it in a config struct + +If you recall from the web servers project, it's common to use a "config" struct to store shared data that HTTP handlers need access to. We'll do the same thing here. Mine looks like this: + +```go +type apiConfig struct { + DB *database.Queries +} +``` + +At the top of `main()` load in your database URL from your `.env` file, and then [.Open()](https://pkg.go.dev/database/sql#Open) a connection to your database: + +```go +db, err := sql.Open("postgres", dbURL) +``` + +Use your generated `database` package to create a new `*database.Queries`, and store it in your config struct: + +```go +dbQueries := database.New(db) +``` + +## 10. Create an HTTP handler to create a user + +Endpoint: `POST /v1/users` + +Example body: + +```json +{ + "name": "Lane" +} +``` + +Example response: + +```json +{ + "id": "3f8805e3-634c-49dd-a347-ab36479f3f83", + "created_at": "2021-09-01T00:00:00Z", + "updated_at": "2021-09-01T00:00:00Z", + "name": "Lane" +} +``` + +Use Google's [UUID](https://pkg.go.dev/github.com/google/uuid) package to generate a new [UUID](https://blog.boot.dev/clean-code/what-are-uuids-and-should-you-use-them/) for the user's ID. Both `created_at` and `updated_at` should be set to the current time. If we ever need to update a user, we'll update the `updated_at` field. + +I'm a fan of a convention where *every table* in my database has: + +* An `id` field that is a UUID (if you're curious why, [read this](https://blog.boot.dev/clean-code/what-are-uuids-and-should-you-use-them/)) +* A `created_at` field that indicates when the row was created +* An `updated_at` field that indicates when the row was last updated + +## 11. Test your handler with an HTTP client! + +C'mon, you know what to do. diff --git a/project/4-create_users/src/.gitignore b/project/4-create_users/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/4-create_users/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/4-create_users/src/go.mod b/project/4-create_users/src/go.mod new file mode 100644 index 0000000..1bade80 --- /dev/null +++ b/project/4-create_users/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/createusers + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/4-create_users/src/go.sum b/project/4-create_users/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/4-create_users/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/4-create_users/src/handler_ready.go b/project/4-create_users/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/4-create_users/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/4-create_users/src/handler_user.go b/project/4-create_users/src/handler_user.go new file mode 100644 index 0000000..37ba6be --- /dev/null +++ b/project/4-create_users/src/handler_user.go @@ -0,0 +1,36 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/createusers/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/4-create_users/src/internal/database/db.go b/project/4-create_users/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/4-create_users/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/4-create_users/src/internal/database/models.go b/project/4-create_users/src/internal/database/models.go new file mode 100644 index 0000000..b971620 --- /dev/null +++ b/project/4-create_users/src/internal/database/models.go @@ -0,0 +1,18 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} diff --git a/project/4-create_users/src/internal/database/users.sql.go b/project/4-create_users/src/internal/database/users.sql.go new file mode 100644 index 0000000..6a3db84 --- /dev/null +++ b/project/4-create_users/src/internal/database/users.sql.go @@ -0,0 +1,43 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name) +VALUES ($1, $2, $3, $4) +RETURNING id, created_at, updated_at, name +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + ) + return i, err +} diff --git a/project/4-create_users/src/json.go b/project/4-create_users/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/4-create_users/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/4-create_users/src/main.go b/project/4-create_users/src/main.go new file mode 100644 index 0000000..7777010 --- /dev/null +++ b/project/4-create_users/src/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/createusers/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/4-create_users/src/models.go b/project/4-create_users/src/models.go new file mode 100644 index 0000000..d400171 --- /dev/null +++ b/project/4-create_users/src/models.go @@ -0,0 +1,24 @@ +package main + +import ( + "time" + + "github.com/bootdotdev/projects/createusers/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + } +} diff --git a/project/4-create_users/src/sql/queries/users.sql b/project/4-create_users/src/sql/queries/users.sql new file mode 100644 index 0000000..39ae8bc --- /dev/null +++ b/project/4-create_users/src/sql/queries/users.sql @@ -0,0 +1,4 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name) +VALUES ($1, $2, $3, $4) +RETURNING *; diff --git a/project/4-create_users/src/sql/schema/001_users.sql b/project/4-create_users/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/4-create_users/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/4-create_users/src/sqlc.yaml b/project/4-create_users/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/4-create_users/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/.gitignore b/project/4-create_users/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/4-create_users/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/4-create_users/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/LICENSE b/project/4-create_users/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/Makefile b/project/4-create_users/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/README.md b/project/4-create_users/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/chain.go b/project/4-create_users/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/chi.go b/project/4-create_users/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/context.go b/project/4-create_users/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/mux.go b/project/4-create_users/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/4-create_users/src/vendor/github.com/go-chi/chi/tree.go b/project/4-create_users/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/4-create_users/src/vendor/github.com/go-chi/cors/LICENSE b/project/4-create_users/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/4-create_users/src/vendor/github.com/go-chi/cors/README.md b/project/4-create_users/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/4-create_users/src/vendor/github.com/go-chi/cors/cors.go b/project/4-create_users/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/4-create_users/src/vendor/github.com/go-chi/cors/utils.go b/project/4-create_users/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/4-create_users/src/vendor/github.com/joho/godotenv/.gitignore b/project/4-create_users/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/4-create_users/src/vendor/github.com/joho/godotenv/LICENCE b/project/4-create_users/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/4-create_users/src/vendor/github.com/joho/godotenv/README.md b/project/4-create_users/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/4-create_users/src/vendor/github.com/joho/godotenv/godotenv.go b/project/4-create_users/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/4-create_users/src/vendor/github.com/joho/godotenv/parser.go b/project/4-create_users/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/4-create_users/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/4-create_users/src/vendor/modules.txt b/project/4-create_users/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/4-create_users/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/5-apikey/readme.md b/project/5-apikey/readme.md new file mode 100644 index 0000000..33cd620 --- /dev/null +++ b/project/5-apikey/readme.md @@ -0,0 +1,56 @@ +# API Key + +## 1. Add an "api key" column to the users table + +Use a new migration file in the `sql/schema` directory to add a new column to the `users` table. I named my file `002_users_apikey.sql`. + +The "up" migration adds the column, and the "down" migration removes it. + +Use a `VARCHAR(64)` that must be unique and not null. Using a string of a specific length does two things: + +1. It ensures we don't accidentally store a key that's too long (type safety) +2. It's more performant than using a variable length `TEXT` column + +Because we're enforcing the `NOT NULL` constraint, and we already have some users in the database, we need to set a default value for the column. A blank default would be a bit silly: that's no better than null! Instead, we'll generate valid API keys (256-bit hex values) using SQL. Here's the function I used: + +```sql +encode(sha256(random()::text::bytea), 'hex') +``` + +When you're done, use `goose postgres CONN up` to perform the migration. + +## 2. Create an API key for new users + +Update your "create user" SQL query to use the same SQL function to generate API keys for new users. + +## 3. Add a new SQL query to get a user by their API key + +This query can live in the same file as the "create user" query, or you can make a new one - it's up to you. + +## 4. Generate new Go code + +Run `sqlc generate` to generate new Go code for your queries. + +## 5. New endpoint: return the current user + +Add a new endpoint that allows users to get their own user information. You'll need to parse the header and use your new query to get the user data. + +Endpoint: `GET /v1/users` + +Request headers: `Authorization: ApiKey ` + +Example response body: + +```json +{ + "id": "3f8805e3-634c-49dd-a347-ab36479f3f83", + "created_at": "2021-09-01T00:00:00Z", + "updated_at": "2021-09-01T00:00:00Z", + "name": "Lane", + "api_key": "cca9688383ceaa25bd605575ac9700da94422aa397ef87e765c8df4438bc9942" +} +``` + +*Test your endpoints with an HTTP client before moving on!* + +Don't forget that each time you update your queries or schema you'll need to regenerate your Go code with `sqlc generate`. If you update the schema you'll also need to migrate your database up (and maybe down). diff --git a/project/5-apikey/src/.gitignore b/project/5-apikey/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/5-apikey/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/5-apikey/src/go.mod b/project/5-apikey/src/go.mod new file mode 100644 index 0000000..df50608 --- /dev/null +++ b/project/5-apikey/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/apikey + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/5-apikey/src/go.sum b/project/5-apikey/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/5-apikey/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/5-apikey/src/handler_ready.go b/project/5-apikey/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/5-apikey/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/5-apikey/src/handler_user.go b/project/5-apikey/src/handler_user.go new file mode 100644 index 0000000..23121b6 --- /dev/null +++ b/project/5-apikey/src/handler_user.go @@ -0,0 +1,55 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/apikey/internal/auth" + "github.com/bootdotdev/projects/apikey/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/5-apikey/src/internal/auth/auth.go b/project/5-apikey/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/5-apikey/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/5-apikey/src/internal/database/db.go b/project/5-apikey/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/5-apikey/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/5-apikey/src/internal/database/models.go b/project/5-apikey/src/internal/database/models.go new file mode 100644 index 0000000..4350b25 --- /dev/null +++ b/project/5-apikey/src/internal/database/models.go @@ -0,0 +1,19 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/5-apikey/src/internal/database/users.sql.go b/project/5-apikey/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/5-apikey/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/5-apikey/src/json.go b/project/5-apikey/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/5-apikey/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/5-apikey/src/main.go b/project/5-apikey/src/main.go new file mode 100644 index 0000000..ce06b50 --- /dev/null +++ b/project/5-apikey/src/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/apikey/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.handlerUsersGet) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/5-apikey/src/models.go b/project/5-apikey/src/models.go new file mode 100644 index 0000000..627b720 --- /dev/null +++ b/project/5-apikey/src/models.go @@ -0,0 +1,26 @@ +package main + +import ( + "time" + + "github.com/bootdotdev/projects/apikey/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} diff --git a/project/5-apikey/src/sql/queries/users.sql b/project/5-apikey/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/5-apikey/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/5-apikey/src/sql/schema/001_users.sql b/project/5-apikey/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/5-apikey/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/5-apikey/src/sql/schema/002_users_apikey.sql b/project/5-apikey/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/5-apikey/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/5-apikey/src/sqlc.yaml b/project/5-apikey/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/5-apikey/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/.gitignore b/project/5-apikey/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/5-apikey/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/5-apikey/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/LICENSE b/project/5-apikey/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/Makefile b/project/5-apikey/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/README.md b/project/5-apikey/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/chain.go b/project/5-apikey/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/chi.go b/project/5-apikey/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/context.go b/project/5-apikey/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/mux.go b/project/5-apikey/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/5-apikey/src/vendor/github.com/go-chi/chi/tree.go b/project/5-apikey/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/5-apikey/src/vendor/github.com/go-chi/cors/LICENSE b/project/5-apikey/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/5-apikey/src/vendor/github.com/go-chi/cors/README.md b/project/5-apikey/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/5-apikey/src/vendor/github.com/go-chi/cors/cors.go b/project/5-apikey/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/5-apikey/src/vendor/github.com/go-chi/cors/utils.go b/project/5-apikey/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/5-apikey/src/vendor/github.com/joho/godotenv/.gitignore b/project/5-apikey/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/5-apikey/src/vendor/github.com/joho/godotenv/LICENCE b/project/5-apikey/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/5-apikey/src/vendor/github.com/joho/godotenv/README.md b/project/5-apikey/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/5-apikey/src/vendor/github.com/joho/godotenv/godotenv.go b/project/5-apikey/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/5-apikey/src/vendor/github.com/joho/godotenv/parser.go b/project/5-apikey/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/5-apikey/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/5-apikey/src/vendor/modules.txt b/project/5-apikey/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/5-apikey/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/6-createfeed/readme.md b/project/6-createfeed/readme.md new file mode 100644 index 0000000..6117013 --- /dev/null +++ b/project/6-createfeed/readme.md @@ -0,0 +1,77 @@ +# Create a Feed + +An RSS feed is just a URL that points to some XML. Users will be able to add feeds to our database so that our server (in a future step) can go download all of the posts in the feed (like blog posts or podcast episodes). + +## 1. Create a feeds table + +Like any table in our DB, we'll need the standard `id`, `created_at`, and `updated_at` fields. We'll also need a few more: + +* `name`: The name of the feed (like "The Changelog, or "The Boot.dev Blog") +* `url`: The URL of the feed +* `user_id`: The ID of the user who added this feed + +I'd recommend making the `url` field unique so that in the future we aren't downloading duplicate posts. I'd also recommend using [ON DELETE CASCADE](https://stackoverflow.com/a/14141354) on the `user_id` foreign key so that if a user is deleted, all of their feeds are automatically deleted as well. + +Write the appropriate migrations and run them. + +## 2. Add a query to create a feed + +Add a new query to create a feed, then use `sqlc generate` to generate the Go code. + +## 3. Create some authentication middleware + +Most of the endpoints going forward will require a user to be logged in. Let's DRY up our code by creating some middleware that will check for a valid API key. + +Now, I'm not a fan of how the Chi router handles stateful middleware using [context](https://pkg.go.dev/context) (middleware that passes data down to the next handler). I prefer to create custom handlers that accept extra values. You can add middleware however you like, but here are some examples from my code. + +### A custom type for handlers that require authentication + +```go +type authedHandler func(http.ResponseWriter, *http.Request, database.User) +``` + +### Middleware that authenticates a request, gets the user and calls the next authed handler + +```go +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + /// +} +``` + +### Using the middleware + +```go +v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) +``` + +## 4. Create a handler to create a feed + +Create a handler that creates a feed. This handler *and* the "get user" handler should use the authentication middleware. + +Endpoint: `POST /v1/feeds` + +Example request body: + +```json +{ + "name": "The Boot.dev Blog", + "url": "https://blog.boot.dev/index.xml" +} +``` + +Example response body: + +```json +{ + "id": "4a82b372-b0e2-45e3-956a-b9b83358f86b", + "created_at": "2021-05-01T00:00:00Z", + "updated_at": "2021-05-01T00:00:00Z", + "name": "The Boot.dev Blog", + "url": "https://blog.boot.dev/index.xml", + "user_id": "d6962597-f316-4306-a929-fe8c8651671e" +} +``` + +## 5. Test + +Test your handler using an HTTP client, then use your database client to make sure the data was saved correctly. diff --git a/project/6-createfeed/src/.gitignore b/project/6-createfeed/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/6-createfeed/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/6-createfeed/src/go.mod b/project/6-createfeed/src/go.mod new file mode 100644 index 0000000..608bf3f --- /dev/null +++ b/project/6-createfeed/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/createfeed + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/6-createfeed/src/go.sum b/project/6-createfeed/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/6-createfeed/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/6-createfeed/src/handler_feed.go b/project/6-createfeed/src/handler_feed.go new file mode 100644 index 0000000..3af5642 --- /dev/null +++ b/project/6-createfeed/src/handler_feed.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/createfeed/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + Name string `json:"name"` + URL string `json:"url"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feed, err := cfg.DB.CreateFeed(r.Context(), database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + Name: params.Name, + Url: params.URL, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedToFeed(feed)) +} diff --git a/project/6-createfeed/src/handler_ready.go b/project/6-createfeed/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/6-createfeed/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/6-createfeed/src/handler_user.go b/project/6-createfeed/src/handler_user.go new file mode 100644 index 0000000..8ecaaf5 --- /dev/null +++ b/project/6-createfeed/src/handler_user.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/createfeed/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/6-createfeed/src/internal/auth/auth.go b/project/6-createfeed/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/6-createfeed/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/6-createfeed/src/internal/database/db.go b/project/6-createfeed/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/6-createfeed/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/6-createfeed/src/internal/database/feeds.sql.go b/project/6-createfeed/src/internal/database/feeds.sql.go new file mode 100644 index 0000000..4dd1b60 --- /dev/null +++ b/project/6-createfeed/src/internal/database/feeds.sql.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feeds.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, created_at, updated_at, name, url, user_id +` + +type CreateFeedParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + arg.Url, + arg.UserID, + ) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ) + return i, err +} diff --git a/project/6-createfeed/src/internal/database/models.go b/project/6-createfeed/src/internal/database/models.go new file mode 100644 index 0000000..ab88875 --- /dev/null +++ b/project/6-createfeed/src/internal/database/models.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type Feed struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/6-createfeed/src/internal/database/users.sql.go b/project/6-createfeed/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/6-createfeed/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/6-createfeed/src/json.go b/project/6-createfeed/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/6-createfeed/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/6-createfeed/src/main.go b/project/6-createfeed/src/main.go new file mode 100644 index 0000000..c00748a --- /dev/null +++ b/project/6-createfeed/src/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/createfeed/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) + + v1Router.Post("/feeds", apiCfg.middlewareAuth(apiCfg.handlerFeedCreate)) + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/6-createfeed/src/middleware_auth.go b/project/6-createfeed/src/middleware_auth.go new file mode 100644 index 0000000..df555ba --- /dev/null +++ b/project/6-createfeed/src/middleware_auth.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/bootdotdev/projects/createfeed/internal/auth" + "github.com/bootdotdev/projects/createfeed/internal/database" +) + +type authedHandler func(http.ResponseWriter, *http.Request, database.User) + +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + handler(w, r, user) + } +} diff --git a/project/6-createfeed/src/models.go b/project/6-createfeed/src/models.go new file mode 100644 index 0000000..ec12f60 --- /dev/null +++ b/project/6-createfeed/src/models.go @@ -0,0 +1,46 @@ +package main + +import ( + "time" + + "github.com/bootdotdev/projects/createfeed/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} + +type Feed struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Url string `json:"url"` + UserID uuid.UUID `json:"user_id"` +} + +func databaseFeedToFeed(feed database.Feed) Feed { + return Feed{ + ID: feed.ID, + CreatedAt: feed.CreatedAt, + UpdatedAt: feed.UpdatedAt, + Name: feed.Name, + Url: feed.Url, + UserID: feed.UserID, + } +} diff --git a/project/6-createfeed/src/sql/queries/feeds.sql b/project/6-createfeed/src/sql/queries/feeds.sql new file mode 100644 index 0000000..570f33b --- /dev/null +++ b/project/6-createfeed/src/sql/queries/feeds.sql @@ -0,0 +1,4 @@ +-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; diff --git a/project/6-createfeed/src/sql/queries/users.sql b/project/6-createfeed/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/6-createfeed/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/6-createfeed/src/sql/schema/001_users.sql b/project/6-createfeed/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/6-createfeed/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/6-createfeed/src/sql/schema/002_users_apikey.sql b/project/6-createfeed/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/6-createfeed/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/6-createfeed/src/sql/schema/003_feeds.sql b/project/6-createfeed/src/sql/schema/003_feeds.sql new file mode 100644 index 0000000..8c9f831 --- /dev/null +++ b/project/6-createfeed/src/sql/schema/003_feeds.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feeds ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE feeds; diff --git a/project/6-createfeed/src/sqlc.yaml b/project/6-createfeed/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/6-createfeed/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/.gitignore b/project/6-createfeed/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/6-createfeed/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/6-createfeed/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/LICENSE b/project/6-createfeed/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/Makefile b/project/6-createfeed/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/README.md b/project/6-createfeed/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/chain.go b/project/6-createfeed/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/chi.go b/project/6-createfeed/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/context.go b/project/6-createfeed/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/mux.go b/project/6-createfeed/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/chi/tree.go b/project/6-createfeed/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/cors/LICENSE b/project/6-createfeed/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/cors/README.md b/project/6-createfeed/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/cors/cors.go b/project/6-createfeed/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/6-createfeed/src/vendor/github.com/go-chi/cors/utils.go b/project/6-createfeed/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/6-createfeed/src/vendor/github.com/joho/godotenv/.gitignore b/project/6-createfeed/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/6-createfeed/src/vendor/github.com/joho/godotenv/LICENCE b/project/6-createfeed/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/6-createfeed/src/vendor/github.com/joho/godotenv/README.md b/project/6-createfeed/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/6-createfeed/src/vendor/github.com/joho/godotenv/godotenv.go b/project/6-createfeed/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/6-createfeed/src/vendor/github.com/joho/godotenv/parser.go b/project/6-createfeed/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/6-createfeed/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/6-createfeed/src/vendor/modules.txt b/project/6-createfeed/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/6-createfeed/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/7-getfeeds/readme.md b/project/7-getfeeds/readme.md new file mode 100644 index 0000000..41cc870 --- /dev/null +++ b/project/7-getfeeds/readme.md @@ -0,0 +1,5 @@ +# Get all feeds + +Create a new endpoint to retrieve *all* of the feeds in the database. This endpoint should *not* require authentication. + +You should be familiar with all of the steps to make this happen by now, use your other endpoints as a reference. diff --git a/project/7-getfeeds/src/.gitignore b/project/7-getfeeds/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/7-getfeeds/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/7-getfeeds/src/go.mod b/project/7-getfeeds/src/go.mod new file mode 100644 index 0000000..abf4684 --- /dev/null +++ b/project/7-getfeeds/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/getfeeds + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/7-getfeeds/src/go.sum b/project/7-getfeeds/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/7-getfeeds/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/7-getfeeds/src/handler_feed.go b/project/7-getfeeds/src/handler_feed.go new file mode 100644 index 0000000..a431145 --- /dev/null +++ b/project/7-getfeeds/src/handler_feed.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/getfeeds/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + Name string `json:"name"` + URL string `json:"url"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feed, err := cfg.DB.CreateFeed(r.Context(), database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + Name: params.Name, + Url: params.URL, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedToFeed(feed)) +} + +func (cfg *apiConfig) handlerGetFeeds(w http.ResponseWriter, r *http.Request) { + feeds, err := cfg.DB.GetFeeds(r.Context()) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get feeds") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedsToFeeds(feeds)) +} diff --git a/project/7-getfeeds/src/handler_ready.go b/project/7-getfeeds/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/7-getfeeds/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/7-getfeeds/src/handler_user.go b/project/7-getfeeds/src/handler_user.go new file mode 100644 index 0000000..618d31f --- /dev/null +++ b/project/7-getfeeds/src/handler_user.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/getfeeds/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/7-getfeeds/src/internal/auth/auth.go b/project/7-getfeeds/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/7-getfeeds/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/7-getfeeds/src/internal/database/db.go b/project/7-getfeeds/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/7-getfeeds/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/7-getfeeds/src/internal/database/feeds.sql.go b/project/7-getfeeds/src/internal/database/feeds.sql.go new file mode 100644 index 0000000..6b40e91 --- /dev/null +++ b/project/7-getfeeds/src/internal/database/feeds.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feeds.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, created_at, updated_at, name, url, user_id +` + +type CreateFeedParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + arg.Url, + arg.UserID, + ) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ) + return i, err +} + +const getFeeds = `-- name: GetFeeds :many +SELECT id, created_at, updated_at, name, url, user_id FROM feeds +` + +func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getFeeds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/7-getfeeds/src/internal/database/models.go b/project/7-getfeeds/src/internal/database/models.go new file mode 100644 index 0000000..ab88875 --- /dev/null +++ b/project/7-getfeeds/src/internal/database/models.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type Feed struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/7-getfeeds/src/internal/database/users.sql.go b/project/7-getfeeds/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/7-getfeeds/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/7-getfeeds/src/json.go b/project/7-getfeeds/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/7-getfeeds/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/7-getfeeds/src/main.go b/project/7-getfeeds/src/main.go new file mode 100644 index 0000000..d5ccc1f --- /dev/null +++ b/project/7-getfeeds/src/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/getfeeds/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) + + v1Router.Post("/feeds", apiCfg.middlewareAuth(apiCfg.handlerFeedCreate)) + v1Router.Get("/feeds", apiCfg.handlerGetFeeds) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/7-getfeeds/src/middleware_auth.go b/project/7-getfeeds/src/middleware_auth.go new file mode 100644 index 0000000..7d45d70 --- /dev/null +++ b/project/7-getfeeds/src/middleware_auth.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/bootdotdev/projects/getfeeds/internal/auth" + "github.com/bootdotdev/projects/getfeeds/internal/database" +) + +type authedHandler func(http.ResponseWriter, *http.Request, database.User) + +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + handler(w, r, user) + } +} diff --git a/project/7-getfeeds/src/models.go b/project/7-getfeeds/src/models.go new file mode 100644 index 0000000..68ef387 --- /dev/null +++ b/project/7-getfeeds/src/models.go @@ -0,0 +1,54 @@ +package main + +import ( + "time" + + "github.com/bootdotdev/projects/getfeeds/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} + +type Feed struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Url string `json:"url"` + UserID uuid.UUID `json:"user_id"` +} + +func databaseFeedToFeed(feed database.Feed) Feed { + return Feed{ + ID: feed.ID, + CreatedAt: feed.CreatedAt, + UpdatedAt: feed.UpdatedAt, + Name: feed.Name, + Url: feed.Url, + UserID: feed.UserID, + } +} + +func databaseFeedsToFeeds(feeds []database.Feed) []Feed { + result := make([]Feed, len(feeds)) + for i, feed := range feeds { + result[i] = databaseFeedToFeed(feed) + } + return result +} diff --git a/project/7-getfeeds/src/sql/queries/feeds.sql b/project/7-getfeeds/src/sql/queries/feeds.sql new file mode 100644 index 0000000..5c4e989 --- /dev/null +++ b/project/7-getfeeds/src/sql/queries/feeds.sql @@ -0,0 +1,7 @@ +-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: GetFeeds :many +SELECT * FROM feeds; diff --git a/project/7-getfeeds/src/sql/queries/users.sql b/project/7-getfeeds/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/7-getfeeds/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/7-getfeeds/src/sql/schema/001_users.sql b/project/7-getfeeds/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/7-getfeeds/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/7-getfeeds/src/sql/schema/002_users_apikey.sql b/project/7-getfeeds/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/7-getfeeds/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/7-getfeeds/src/sql/schema/003_feeds.sql b/project/7-getfeeds/src/sql/schema/003_feeds.sql new file mode 100644 index 0000000..8c9f831 --- /dev/null +++ b/project/7-getfeeds/src/sql/schema/003_feeds.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feeds ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE feeds; diff --git a/project/7-getfeeds/src/sqlc.yaml b/project/7-getfeeds/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/7-getfeeds/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/.gitignore b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/LICENSE b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/Makefile b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/README.md b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chain.go b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chi.go b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/context.go b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/mux.go b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/chi/tree.go b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/cors/LICENSE b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/cors/README.md b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/cors/cors.go b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/7-getfeeds/src/vendor/github.com/go-chi/cors/utils.go b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/7-getfeeds/src/vendor/github.com/joho/godotenv/.gitignore b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/7-getfeeds/src/vendor/github.com/joho/godotenv/LICENCE b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/7-getfeeds/src/vendor/github.com/joho/godotenv/README.md b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/7-getfeeds/src/vendor/github.com/joho/godotenv/godotenv.go b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/7-getfeeds/src/vendor/github.com/joho/godotenv/parser.go b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/7-getfeeds/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/7-getfeeds/src/vendor/modules.txt b/project/7-getfeeds/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/7-getfeeds/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/8-feedfollows/readme.md b/project/8-feedfollows/readme.md new file mode 100644 index 0000000..7ddadb0 --- /dev/null +++ b/project/8-feedfollows/readme.md @@ -0,0 +1,87 @@ +# Feed Follows + +Aside from just adding new feeds to the database, users can specify *which* feeds they want to follow. This will be important later when we want to show users a list of posts from the feeds they follow. + +Add support for the following endpoints, and update the "create feed" endpoint as specified below. + +## What is a "feed follow"? + +A feed follow is just a link between a user and a feed. It's a [many-to-many](https://en.wikipedia.org/wiki/Many-to-many_(data_model)) relationship, so a user can follow many feeds, and a feed can be followed by many users. + +Creating a feed follow indicates that a user is now following a feed. Deleting it is the same as "unfollowing" a feed. + +It's important to understand that the `ID` of a feed follow is not the same as the `ID` of the feed itself. Each user/feed pair will have a unique feed follow id. + +## Create a feed follow + +Endpoint: `POST /v1/feed_follows` + +*Requires authentication* + +Example request body: + +```json +{ + "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b" +} +``` + +Example response body: + +```json +{ + "id": "c834c69e-ee26-4c63-a677-a977432f9cfa", + "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b", + "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", + "created_at": "2017-01-01T00:00:00Z", + "updated_at": "2017-01-01T00:00:00Z" +} +``` + +## Delete a feed follow + +Endpoint: `DELETE /v1/feed_follows/{feedFollowID}` + +## Get all feed follows for a user + +Endpoint: `GET /v1/feed_follows` + +*Requires authentication* + +Example response: + +```json +[ + { + "id": "c834c69e-ee26-4c63-a677-a977432f9cfa", + "feed_id": "4a82b372-b0e2-45e3-956a-b9b83358f86b", + "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", + "created_at": "2017-01-01T00:00:00Z", + "updated_at": "2017-01-01T00:00:00Z" + }, + { + "id": "ad752167-f509-4ff3-8425-7781090b5c8f", + "feed_id": "f71b842d-9fd1-4bc0-9913-dd96ba33bb15", + "user_id": "0e4fecc6-1354-47b8-8336-2077b307b20e", + "created_at": "2017-01-01T00:00:00Z", + "updated_at": "2017-01-01T00:00:00Z" + } +] +``` + +## Automatically create a feed follow when creating a feed + +When a user creates a new feed, they should automatically be following that feed. They can of course choose to unfollow it later, but it should be there by default. + +The response of this endpoint should now contain both entities: + +```json +{ + "feed": { the feed object }, + "feed_follow": { the feed follow object } +} +``` + +## Test + +As always, test all of your endpoints and make sure they work. Additionally, make sure that they return the proper error codes when they receive invalid inputs. diff --git a/project/8-feedfollows/src/.gitignore b/project/8-feedfollows/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/8-feedfollows/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/8-feedfollows/src/go.mod b/project/8-feedfollows/src/go.mod new file mode 100644 index 0000000..a6aed49 --- /dev/null +++ b/project/8-feedfollows/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/feedfollows + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/8-feedfollows/src/go.sum b/project/8-feedfollows/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/8-feedfollows/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/8-feedfollows/src/handler_feed.go b/project/8-feedfollows/src/handler_feed.go new file mode 100644 index 0000000..5429c10 --- /dev/null +++ b/project/8-feedfollows/src/handler_feed.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/feedfollows/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + Name string `json:"name"` + URL string `json:"url"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feed, err := cfg.DB.CreateFeed(r.Context(), database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + Name: params.Name, + Url: params.URL, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: feed.ID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct { + feed Feed + feedFollow FeedFollow + }{ + feed: databaseFeedToFeed(feed), + feedFollow: databaseFeedFollowToFeedFollow(feedFollow), + }) +} + +func (cfg *apiConfig) handlerGetFeeds(w http.ResponseWriter, r *http.Request) { + feeds, err := cfg.DB.GetFeeds(r.Context()) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get feeds") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedsToFeeds(feeds)) +} diff --git a/project/8-feedfollows/src/handler_feed_follows.go b/project/8-feedfollows/src/handler_feed_follows.go new file mode 100644 index 0000000..2f5d204 --- /dev/null +++ b/project/8-feedfollows/src/handler_feed_follows.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/feedfollows/internal/database" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedFollowsGet(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollows, err := cfg.DB.GetFeedFollowsForUser(r.Context(), user.ID) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowsToFeedFollows(feedFollows)) +} + +func (cfg *apiConfig) handlerFeedFollowCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + FeedID uuid.UUID + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: params.FeedID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowToFeedFollow(feedFollow)) +} + +func (cfg *apiConfig) handlerFeedFollowDelete(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollowIDStr := chi.URLParam(r, "feedFollowID") + feedFollowID, err := uuid.Parse(feedFollowIDStr) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid feed follow ID") + return + } + + err = cfg.DB.DeleteFeedFollow(r.Context(), database.DeleteFeedFollowParams{ + UserID: user.ID, + ID: feedFollowID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct{}{}) +} diff --git a/project/8-feedfollows/src/handler_ready.go b/project/8-feedfollows/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/8-feedfollows/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/8-feedfollows/src/handler_user.go b/project/8-feedfollows/src/handler_user.go new file mode 100644 index 0000000..b064d52 --- /dev/null +++ b/project/8-feedfollows/src/handler_user.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/feedfollows/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/8-feedfollows/src/internal/auth/auth.go b/project/8-feedfollows/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/8-feedfollows/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/8-feedfollows/src/internal/database/db.go b/project/8-feedfollows/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/8-feedfollows/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/8-feedfollows/src/internal/database/feed_follows.sql.go b/project/8-feedfollows/src/internal/database/feed_follows.sql.go new file mode 100644 index 0000000..12ce1e8 --- /dev/null +++ b/project/8-feedfollows/src/internal/database/feed_follows.sql.go @@ -0,0 +1,95 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feed_follows.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeedFollow = `-- name: CreateFeedFollow :one + +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, created_at, updated_at, user_id, feed_id +` + +type CreateFeedFollowParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (FeedFollow, error) { + row := q.db.QueryRowContext(ctx, createFeedFollow, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.UserID, + arg.FeedID, + ) + var i FeedFollow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ) + return i, err +} + +const deleteFeedFollow = `-- name: DeleteFeedFollow :exec + +DELETE FROM feed_follows WHERE id = $1 and user_id = $2 +` + +type DeleteFeedFollowParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error { + _, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.ID, arg.UserID) + return err +} + +const getFeedFollowsForUser = `-- name: GetFeedFollowsForUser :many +SELECT id, created_at, updated_at, user_id, feed_id FROM feed_follows WHERE user_id = $1 +` + +func (q *Queries) GetFeedFollowsForUser(ctx context.Context, userID uuid.UUID) ([]FeedFollow, error) { + rows, err := q.db.QueryContext(ctx, getFeedFollowsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FeedFollow + for rows.Next() { + var i FeedFollow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/8-feedfollows/src/internal/database/feeds.sql.go b/project/8-feedfollows/src/internal/database/feeds.sql.go new file mode 100644 index 0000000..6b40e91 --- /dev/null +++ b/project/8-feedfollows/src/internal/database/feeds.sql.go @@ -0,0 +1,83 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feeds.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, created_at, updated_at, name, url, user_id +` + +type CreateFeedParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + arg.Url, + arg.UserID, + ) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ) + return i, err +} + +const getFeeds = `-- name: GetFeeds :many +SELECT id, created_at, updated_at, name, url, user_id FROM feeds +` + +func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getFeeds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/8-feedfollows/src/internal/database/models.go b/project/8-feedfollows/src/internal/database/models.go new file mode 100644 index 0000000..fb140a1 --- /dev/null +++ b/project/8-feedfollows/src/internal/database/models.go @@ -0,0 +1,36 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "time" + + "github.com/google/uuid" +) + +type Feed struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +type FeedFollow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/8-feedfollows/src/internal/database/users.sql.go b/project/8-feedfollows/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/8-feedfollows/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/8-feedfollows/src/json.go b/project/8-feedfollows/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/8-feedfollows/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/8-feedfollows/src/main.go b/project/8-feedfollows/src/main.go new file mode 100644 index 0000000..3011f7e --- /dev/null +++ b/project/8-feedfollows/src/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/feedfollows/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) + + v1Router.Post("/feeds", apiCfg.middlewareAuth(apiCfg.handlerFeedCreate)) + v1Router.Get("/feeds", apiCfg.handlerGetFeeds) + + v1Router.Get("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowsGet)) + v1Router.Post("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowCreate)) + v1Router.Delete("/feed_follows/{feedFollowID}", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowDelete)) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/8-feedfollows/src/middleware_auth.go b/project/8-feedfollows/src/middleware_auth.go new file mode 100644 index 0000000..d7fd583 --- /dev/null +++ b/project/8-feedfollows/src/middleware_auth.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/bootdotdev/projects/feedfollows/internal/auth" + "github.com/bootdotdev/projects/feedfollows/internal/database" +) + +type authedHandler func(http.ResponseWriter, *http.Request, database.User) + +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + handler(w, r, user) + } +} diff --git a/project/8-feedfollows/src/models.go b/project/8-feedfollows/src/models.go new file mode 100644 index 0000000..e2a7a8d --- /dev/null +++ b/project/8-feedfollows/src/models.go @@ -0,0 +1,80 @@ +package main + +import ( + "time" + + "github.com/bootdotdev/projects/feedfollows/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} + +type Feed struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Url string `json:"url"` + UserID uuid.UUID `json:"user_id"` +} + +func databaseFeedToFeed(feed database.Feed) Feed { + return Feed{ + ID: feed.ID, + CreatedAt: feed.CreatedAt, + UpdatedAt: feed.UpdatedAt, + Name: feed.Name, + Url: feed.Url, + UserID: feed.UserID, + } +} + +func databaseFeedsToFeeds(feeds []database.Feed) []Feed { + result := make([]Feed, len(feeds)) + for i, feed := range feeds { + result[i] = databaseFeedToFeed(feed) + } + return result +} + +type FeedFollow struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID uuid.UUID `json:"user_id"` + FeedID uuid.UUID `json:"feed_id"` +} + +func databaseFeedFollowToFeedFollow(feedFollow database.FeedFollow) FeedFollow { + return FeedFollow{ + ID: feedFollow.ID, + CreatedAt: feedFollow.CreatedAt, + UpdatedAt: feedFollow.UpdatedAt, + UserID: feedFollow.UserID, + FeedID: feedFollow.FeedID, + } +} + +func databaseFeedFollowsToFeedFollows(feedFollows []database.FeedFollow) []FeedFollow { + result := make([]FeedFollow, len(feedFollows)) + for i, feedFollow := range feedFollows { + result[i] = databaseFeedFollowToFeedFollow(feedFollow) + } + return result +} diff --git a/project/8-feedfollows/src/sql/queries/feed_follows.sql b/project/8-feedfollows/src/sql/queries/feed_follows.sql new file mode 100644 index 0000000..ebef9a9 --- /dev/null +++ b/project/8-feedfollows/src/sql/queries/feed_follows.sql @@ -0,0 +1,13 @@ +-- name: GetFeedFollowsForUser :many +SELECT * FROM feed_follows WHERE user_id = $1; +-- + +-- name: CreateFeedFollow :one +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING *; +-- + +-- name: DeleteFeedFollow :exec +DELETE FROM feed_follows WHERE id = $1 and user_id = $2; +-- diff --git a/project/8-feedfollows/src/sql/queries/feeds.sql b/project/8-feedfollows/src/sql/queries/feeds.sql new file mode 100644 index 0000000..5c4e989 --- /dev/null +++ b/project/8-feedfollows/src/sql/queries/feeds.sql @@ -0,0 +1,7 @@ +-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: GetFeeds :many +SELECT * FROM feeds; diff --git a/project/8-feedfollows/src/sql/queries/users.sql b/project/8-feedfollows/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/8-feedfollows/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/8-feedfollows/src/sql/schema/001_users.sql b/project/8-feedfollows/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/8-feedfollows/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/8-feedfollows/src/sql/schema/002_users_apikey.sql b/project/8-feedfollows/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/8-feedfollows/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/8-feedfollows/src/sql/schema/003_feeds.sql b/project/8-feedfollows/src/sql/schema/003_feeds.sql new file mode 100644 index 0000000..8c9f831 --- /dev/null +++ b/project/8-feedfollows/src/sql/schema/003_feeds.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feeds ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE feeds; diff --git a/project/8-feedfollows/src/sql/schema/004_feed_follows.sql b/project/8-feedfollows/src/sql/schema/004_feed_follows.sql new file mode 100644 index 0000000..f5e108b --- /dev/null +++ b/project/8-feedfollows/src/sql/schema/004_feed_follows.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feed_follows ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + feed_id UUID NOT NULL REFERENCES feeds(id) ON DELETE CASCADE, + UNIQUE (user_id, feed_id) +); + +-- +goose Down +DROP TABLE feed_follows; diff --git a/project/8-feedfollows/src/sqlc.yaml b/project/8-feedfollows/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/8-feedfollows/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/.gitignore b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/LICENSE b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/Makefile b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/README.md b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chain.go b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chi.go b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/context.go b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/mux.go b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/chi/tree.go b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/cors/LICENSE b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/cors/README.md b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/cors/cors.go b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/8-feedfollows/src/vendor/github.com/go-chi/cors/utils.go b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/8-feedfollows/src/vendor/github.com/joho/godotenv/.gitignore b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/8-feedfollows/src/vendor/github.com/joho/godotenv/LICENCE b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/8-feedfollows/src/vendor/github.com/joho/godotenv/README.md b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/8-feedfollows/src/vendor/github.com/joho/godotenv/godotenv.go b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/8-feedfollows/src/vendor/github.com/joho/godotenv/parser.go b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/8-feedfollows/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/8-feedfollows/src/vendor/modules.txt b/project/8-feedfollows/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/8-feedfollows/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv diff --git a/project/9-scraper/readme.md b/project/9-scraper/readme.md new file mode 100644 index 0000000..f055203 --- /dev/null +++ b/project/9-scraper/readme.md @@ -0,0 +1,59 @@ +# Scraper + +This is going to be a fairly large step. I recommend breaking it down into smaller pieces and functions, and testing each piece as you go. + +Here are some different strategies I use depending on the situation: + +* Write a unit test for a function that has simple inputs and outputs +* Edit `main.go` to call a function so I can quickly test it by running the whole program. Remove the call after testing and plug it into its proper place +* Put the code in a package, then write a separate `main` package (just a little `main()` script) that I can use to independently test the code in the package + +*Commit your code each time you get a new piece working.* + +## Add a `last_fetched_at` column to the `feeds` table + +We need to keep track of when we last fetched the posts from a feed. This should be a nullable timestamp. + +The `sql.NullTime` type is useful for nullable timestamps on the database side, but it's not great for marshaling into JSON. It results in a weird nested object. I'd recommend converting it to a `*time.Time` before returning it across the HTTP response. + +I map all of my database structs to a different struct that has the intended JSON structure. This is a good way to keep your database and HTTP APIs separate. + +For example: `func databaseFeedToFeed(feed database.Feed) Feed` + +## Add `GetNextFeedsToFetch()` query to the database + +It should return the next `n` feeds that need to be fetched, ordered by `last_fetched_at`, but with `NULL` values first. We obviously want to fetch the feeds that have never been fetched before or the ones that were fetched the longest time ago. + +## Add a `MarkFeedFetched()` query to the database + +It should update a feed and set its `last_fetched_at` to the current time. Don't forget to also update the `updated_at` field because we've updated the record. + +## Write a function that can fetch data from a feed + +This function should accept the URL of a live RSS feed, and return the parsed data in a Go struct. + +You can test with these ones: + +* `https://blog.boot.dev/index.xml` +* `https://wagslane.com/feed.xml` + +And any other blogs you enjoy that have RSS feeds. + +*Please be careful not to [DDOS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) any of the sites you're fetching from. Don't send too many requests!* + +You can parse the returned XML with the [encoding/xml](https://pkg.go.dev/encoding/xml) package, it works *very* similarly to `encoding/json`. Define the structure of an RSS feed as a Go struct, then unmarshal the XML into that struct. + +## Write a worker that fetches feeds continuously + +This function should, on an interval (say every 60 seconds or so): + +* Get the next `n` feeds to fetch from the database (you can configure `n`, I used `10`) +* Fetch and process all the feeds *at the same time* (you can use [sync.WaitGroup](https://pkg.go.dev/sync#WaitGroup) for this) + +For now, "process" the feed by simply printing out the titles of each post + +I recommend adding a lot of logging messages to this worker so that as it runs you can see what it's doing! + +## Call your worker from `main.go` + +Be sure to start the worker in its own goroutine, so that it runs in the background and processes feeds even as it simultaneously handles new HTTP requests. diff --git a/project/9-scraper/src/.gitignore b/project/9-scraper/src/.gitignore new file mode 100644 index 0000000..99dc9b9 --- /dev/null +++ b/project/9-scraper/src/.gitignore @@ -0,0 +1,2 @@ +out +.env diff --git a/project/9-scraper/src/go.mod b/project/9-scraper/src/go.mod new file mode 100644 index 0000000..d436bd7 --- /dev/null +++ b/project/9-scraper/src/go.mod @@ -0,0 +1,9 @@ +module github.com/bootdotdev/projects/scraper + +go 1.20 + +require ( + github.com/go-chi/chi v1.5.4 + github.com/go-chi/cors v1.2.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/project/9-scraper/src/go.sum b/project/9-scraper/src/go.sum new file mode 100644 index 0000000..615d34a --- /dev/null +++ b/project/9-scraper/src/go.sum @@ -0,0 +1,6 @@ +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/project/9-scraper/src/handler_feed.go b/project/9-scraper/src/handler_feed.go new file mode 100644 index 0000000..81a6052 --- /dev/null +++ b/project/9-scraper/src/handler_feed.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/scraper/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + Name string `json:"name"` + URL string `json:"url"` + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feed, err := cfg.DB.CreateFeed(r.Context(), database.CreateFeedParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + Name: params.Name, + Url: params.URL, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: feed.ID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct { + feed Feed + feedFollow FeedFollow + }{ + feed: databaseFeedToFeed(feed), + feedFollow: databaseFeedFollowToFeedFollow(feedFollow), + }) +} + +func (cfg *apiConfig) handlerGetFeeds(w http.ResponseWriter, r *http.Request) { + feeds, err := cfg.DB.GetFeeds(r.Context()) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't get feeds") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedsToFeeds(feeds)) +} diff --git a/project/9-scraper/src/handler_feed_follows.go b/project/9-scraper/src/handler_feed_follows.go new file mode 100644 index 0000000..077fd27 --- /dev/null +++ b/project/9-scraper/src/handler_feed_follows.go @@ -0,0 +1,68 @@ +package main + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/bootdotdev/projects/scraper/internal/database" + "github.com/go-chi/chi" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerFeedFollowsGet(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollows, err := cfg.DB.GetFeedFollowsForUser(r.Context(), user.ID) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowsToFeedFollows(feedFollows)) +} + +func (cfg *apiConfig) handlerFeedFollowCreate(w http.ResponseWriter, r *http.Request, user database.User) { + type parameters struct { + FeedID uuid.UUID + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + feedFollow, err := cfg.DB.CreateFeedFollow(r.Context(), database.CreateFeedFollowParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + UserID: user.ID, + FeedID: params.FeedID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, databaseFeedFollowToFeedFollow(feedFollow)) +} + +func (cfg *apiConfig) handlerFeedFollowDelete(w http.ResponseWriter, r *http.Request, user database.User) { + feedFollowIDStr := chi.URLParam(r, "feedFollowID") + feedFollowID, err := uuid.Parse(feedFollowIDStr) + if err != nil { + respondWithError(w, http.StatusBadRequest, "Invalid feed follow ID") + return + } + + err = cfg.DB.DeleteFeedFollow(r.Context(), database.DeleteFeedFollowParams{ + UserID: user.ID, + ID: feedFollowID, + }) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't create feed follow") + return + } + + respondWithJSON(w, http.StatusOK, struct{}{}) +} diff --git a/project/9-scraper/src/handler_ready.go b/project/9-scraper/src/handler_ready.go new file mode 100644 index 0000000..65b8f1c --- /dev/null +++ b/project/9-scraper/src/handler_ready.go @@ -0,0 +1,11 @@ +package main + +import "net/http" + +func handlerReadiness(w http.ResponseWriter, r *http.Request) { + respondWithJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func handlerErr(w http.ResponseWriter, r *http.Request) { + respondWithError(w, http.StatusInternalServerError, "Internal Server Error") +} diff --git a/project/9-scraper/src/handler_user.go b/project/9-scraper/src/handler_user.go new file mode 100644 index 0000000..9e573bb --- /dev/null +++ b/project/9-scraper/src/handler_user.go @@ -0,0 +1,42 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/bootdotdev/projects/scraper/internal/database" + "github.com/google/uuid" +) + +func (cfg *apiConfig) handlerUsersCreate(w http.ResponseWriter, r *http.Request) { + type parameters struct { + Name string + } + decoder := json.NewDecoder(r.Body) + params := parameters{} + err := decoder.Decode(¶ms) + if err != nil { + respondWithError(w, http.StatusInternalServerError, "Couldn't decode parameters") + return + } + + user, err := cfg.DB.CreateUser(r.Context(), database.CreateUserParams{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + Name: params.Name, + }) + if err != nil { + log.Println(err) + respondWithError(w, http.StatusInternalServerError, "Couldn't create user") + return + } + + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} + +func (cfg *apiConfig) handlerUsersGet(w http.ResponseWriter, r *http.Request, user database.User) { + respondWithJSON(w, http.StatusOK, databaseUserToUser(user)) +} diff --git a/project/9-scraper/src/internal/auth/auth.go b/project/9-scraper/src/internal/auth/auth.go new file mode 100644 index 0000000..f969aac --- /dev/null +++ b/project/9-scraper/src/internal/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + +// GetAPIKey - +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + + return splitAuth[1], nil +} diff --git a/project/9-scraper/src/internal/database/db.go b/project/9-scraper/src/internal/database/db.go new file mode 100644 index 0000000..96fd5b3 --- /dev/null +++ b/project/9-scraper/src/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/project/9-scraper/src/internal/database/feed_follows.sql.go b/project/9-scraper/src/internal/database/feed_follows.sql.go new file mode 100644 index 0000000..12ce1e8 --- /dev/null +++ b/project/9-scraper/src/internal/database/feed_follows.sql.go @@ -0,0 +1,95 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feed_follows.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeedFollow = `-- name: CreateFeedFollow :one + +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING id, created_at, updated_at, user_id, feed_id +` + +type CreateFeedFollowParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +func (q *Queries) CreateFeedFollow(ctx context.Context, arg CreateFeedFollowParams) (FeedFollow, error) { + row := q.db.QueryRowContext(ctx, createFeedFollow, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.UserID, + arg.FeedID, + ) + var i FeedFollow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ) + return i, err +} + +const deleteFeedFollow = `-- name: DeleteFeedFollow :exec + +DELETE FROM feed_follows WHERE id = $1 and user_id = $2 +` + +type DeleteFeedFollowParams struct { + ID uuid.UUID + UserID uuid.UUID +} + +func (q *Queries) DeleteFeedFollow(ctx context.Context, arg DeleteFeedFollowParams) error { + _, err := q.db.ExecContext(ctx, deleteFeedFollow, arg.ID, arg.UserID) + return err +} + +const getFeedFollowsForUser = `-- name: GetFeedFollowsForUser :many +SELECT id, created_at, updated_at, user_id, feed_id FROM feed_follows WHERE user_id = $1 +` + +func (q *Queries) GetFeedFollowsForUser(ctx context.Context, userID uuid.UUID) ([]FeedFollow, error) { + rows, err := q.db.QueryContext(ctx, getFeedFollowsForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FeedFollow + for rows.Next() { + var i FeedFollow + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.UserID, + &i.FeedID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/project/9-scraper/src/internal/database/feeds.sql.go b/project/9-scraper/src/internal/database/feeds.sql.go new file mode 100644 index 0000000..449647c --- /dev/null +++ b/project/9-scraper/src/internal/database/feeds.sql.go @@ -0,0 +1,145 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: feeds.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createFeed = `-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at +` + +type CreateFeedParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID +} + +func (q *Queries) CreateFeed(ctx context.Context, arg CreateFeedParams) (Feed, error) { + row := q.db.QueryRowContext(ctx, createFeed, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + arg.Url, + arg.UserID, + ) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ) + return i, err +} + +const getFeeds = `-- name: GetFeeds :many +SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds +` + +func (q *Queries) GetFeeds(ctx context.Context) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getFeeds) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getNextFeedsToFetch = `-- name: GetNextFeedsToFetch :many +SELECT id, created_at, updated_at, name, url, user_id, last_fetched_at FROM feeds +ORDER BY last_fetched_at ASC NULLS FIRST +LIMIT $1 +` + +func (q *Queries) GetNextFeedsToFetch(ctx context.Context, limit int32) ([]Feed, error) { + rows, err := q.db.QueryContext(ctx, getNextFeedsToFetch, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Feed + for rows.Next() { + var i Feed + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const markFeedFetched = `-- name: MarkFeedFetched :one +UPDATE feeds +SET last_fetched_at = NOW(), +updated_at = NOW() +WHERE id = $1 +RETURNING id, created_at, updated_at, name, url, user_id, last_fetched_at +` + +func (q *Queries) MarkFeedFetched(ctx context.Context, id uuid.UUID) (Feed, error) { + row := q.db.QueryRowContext(ctx, markFeedFetched, id) + var i Feed + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.Url, + &i.UserID, + &i.LastFetchedAt, + ) + return i, err +} diff --git a/project/9-scraper/src/internal/database/models.go b/project/9-scraper/src/internal/database/models.go new file mode 100644 index 0000000..dfee755 --- /dev/null +++ b/project/9-scraper/src/internal/database/models.go @@ -0,0 +1,38 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 + +package database + +import ( + "database/sql" + "time" + + "github.com/google/uuid" +) + +type Feed struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + Url string + UserID uuid.UUID + LastFetchedAt sql.NullTime +} + +type FeedFollow struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + UserID uuid.UUID + FeedID uuid.UUID +} + +type User struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string + ApiKey string +} diff --git a/project/9-scraper/src/internal/database/users.sql.go b/project/9-scraper/src/internal/database/users.sql.go new file mode 100644 index 0000000..7ee87ba --- /dev/null +++ b/project/9-scraper/src/internal/database/users.sql.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.17.2 +// source: users.sql + +package database + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING id, created_at, updated_at, name, api_key +` + +type CreateUserParams struct { + ID uuid.UUID + CreatedAt time.Time + UpdatedAt time.Time + Name string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRowContext(ctx, createUser, + arg.ID, + arg.CreatedAt, + arg.UpdatedAt, + arg.Name, + ) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} + +const getUserByAPIKey = `-- name: GetUserByAPIKey :one +SELECT id, created_at, updated_at, name, api_key FROM users WHERE api_key = $1 +` + +func (q *Queries) GetUserByAPIKey(ctx context.Context, apiKey string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByAPIKey, apiKey) + var i User + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.ApiKey, + ) + return i, err +} diff --git a/project/9-scraper/src/json.go b/project/9-scraper/src/json.go new file mode 100644 index 0000000..e346ef4 --- /dev/null +++ b/project/9-scraper/src/json.go @@ -0,0 +1,31 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" +) + +func respondWithError(w http.ResponseWriter, code int, msg string) { + if code > 499 { + log.Printf("Responding with 5XX error: %s", msg) + } + type errorResponse struct { + Error string `json:"error"` + } + respondWithJSON(w, code, errorResponse{ + Error: msg, + }) +} + +func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) + if err != nil { + log.Printf("Error marshalling JSON: %s", err) + w.WriteHeader(500) + return + } + w.WriteHeader(code) + w.Write(dat) +} diff --git a/project/9-scraper/src/main.go b/project/9-scraper/src/main.go new file mode 100644 index 0000000..8ddb89e --- /dev/null +++ b/project/9-scraper/src/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "database/sql" + "log" + "net/http" + "os" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/cors" + "github.com/joho/godotenv" + + "github.com/bootdotdev/projects/scraper/internal/database" + + _ "github.com/lib/pq" +) + +type apiConfig struct { + DB *database.Queries +} + +func main() { + godotenv.Load(".env") + + port := os.Getenv("PORT") + if port == "" { + log.Fatal("PORT environment variable is not set") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + log.Fatal("DATABASE_URL environment variable is not set") + } + + db, err := sql.Open("postgres", dbURL) + if err != nil { + log.Fatal(err) + } + dbQueries := database.New(db) + + apiCfg := apiConfig{ + DB: dbQueries, + } + + router := chi.NewRouter() + + router.Use(cors.Handler(cors.Options{ + AllowedOrigins: []string{"https://*", "http://*"}, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"*"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, + })) + + v1Router := chi.NewRouter() + + v1Router.Post("/users", apiCfg.handlerUsersCreate) + v1Router.Get("/users", apiCfg.middlewareAuth(apiCfg.handlerUsersGet)) + + v1Router.Post("/feeds", apiCfg.middlewareAuth(apiCfg.handlerFeedCreate)) + v1Router.Get("/feeds", apiCfg.handlerGetFeeds) + + v1Router.Get("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowsGet)) + v1Router.Post("/feed_follows", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowCreate)) + v1Router.Delete("/feed_follows/{feedFollowID}", apiCfg.middlewareAuth(apiCfg.handlerFeedFollowDelete)) + + v1Router.Get("/healthz", handlerReadiness) + v1Router.Get("/err", handlerErr) + + router.Mount("/v1", v1Router) + srv := &http.Server{ + Addr: ":" + port, + Handler: router, + } + + const collectionConcurrency = 10 + const collectionInterval = time.Minute + go startScraping(dbQueries, collectionConcurrency, collectionInterval) + + log.Printf("Serving on port: %s\n", port) + log.Fatal(srv.ListenAndServe()) +} diff --git a/project/9-scraper/src/middleware_auth.go b/project/9-scraper/src/middleware_auth.go new file mode 100644 index 0000000..f7a69e2 --- /dev/null +++ b/project/9-scraper/src/middleware_auth.go @@ -0,0 +1,28 @@ +package main + +import ( + "net/http" + + "github.com/bootdotdev/projects/scraper/internal/auth" + "github.com/bootdotdev/projects/scraper/internal/database" +) + +type authedHandler func(http.ResponseWriter, *http.Request, database.User) + +func (cfg *apiConfig) middlewareAuth(handler authedHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + apiKey, err := auth.GetAPIKey(r.Header) + if err != nil { + respondWithError(w, http.StatusUnauthorized, "Couldn't find api key") + return + } + + user, err := cfg.DB.GetUserByAPIKey(r.Context(), apiKey) + if err != nil { + respondWithError(w, http.StatusNotFound, "Couldn't get user") + return + } + + handler(w, r, user) + } +} diff --git a/project/9-scraper/src/models.go b/project/9-scraper/src/models.go new file mode 100644 index 0000000..48f8675 --- /dev/null +++ b/project/9-scraper/src/models.go @@ -0,0 +1,90 @@ +package main + +import ( + "database/sql" + "time" + + "github.com/bootdotdev/projects/scraper/internal/database" + "github.com/google/uuid" +) + +type User struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + ApiKey string `json:"api_key"` +} + +func databaseUserToUser(user database.User) User { + return User{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + ApiKey: user.ApiKey, + } +} + +type Feed struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Name string `json:"name"` + Url string `json:"url"` + UserID uuid.UUID `json:"user_id"` + LastFetchedAt *time.Time `json:"last_fetched_at"` +} + +func databaseFeedToFeed(feed database.Feed) Feed { + return Feed{ + ID: feed.ID, + CreatedAt: feed.CreatedAt, + UpdatedAt: feed.UpdatedAt, + Name: feed.Name, + Url: feed.Url, + UserID: feed.UserID, + LastFetchedAt: nullTimeToTimePtr(feed.LastFetchedAt), + } +} + +func databaseFeedsToFeeds(feeds []database.Feed) []Feed { + result := make([]Feed, len(feeds)) + for i, feed := range feeds { + result[i] = databaseFeedToFeed(feed) + } + return result +} + +type FeedFollow struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID uuid.UUID `json:"user_id"` + FeedID uuid.UUID `json:"feed_id"` +} + +func databaseFeedFollowToFeedFollow(feedFollow database.FeedFollow) FeedFollow { + return FeedFollow{ + ID: feedFollow.ID, + CreatedAt: feedFollow.CreatedAt, + UpdatedAt: feedFollow.UpdatedAt, + UserID: feedFollow.UserID, + FeedID: feedFollow.FeedID, + } +} + +func databaseFeedFollowsToFeedFollows(feedFollows []database.FeedFollow) []FeedFollow { + result := make([]FeedFollow, len(feedFollows)) + for i, feedFollow := range feedFollows { + result[i] = databaseFeedFollowToFeedFollow(feedFollow) + } + return result +} + +func nullTimeToTimePtr(t sql.NullTime) *time.Time { + if t.Valid { + return &t.Time + } + return nil +} diff --git a/project/9-scraper/src/scraper.go b/project/9-scraper/src/scraper.go new file mode 100644 index 0000000..a75a9ab --- /dev/null +++ b/project/9-scraper/src/scraper.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "encoding/xml" + "io" + "log" + "net/http" + "sync" + "time" + + "github.com/bootdotdev/projects/scraper/internal/database" +) + +func startScraping(db *database.Queries, concurrency int, timeBetweenRequest time.Duration) { + log.Printf("Collecting feeds every %s on %v goroutines...", timeBetweenRequest, concurrency) + ticker := time.NewTicker(timeBetweenRequest) + + for ; ; <-ticker.C { + feeds, err := db.GetNextFeedsToFetch(context.Background(), int32(concurrency)) + if err != nil { + log.Println("Couldn't get next feeds to fetch", err) + continue + } + log.Printf("Found %v feeds to fetch!", len(feeds)) + + wg := &sync.WaitGroup{} + for _, feed := range feeds { + wg.Add(1) + go scrapeFeed(db, wg, feed) + } + wg.Wait() + } +} + +func scrapeFeed(db *database.Queries, wg *sync.WaitGroup, feed database.Feed) { + defer wg.Done() + _, err := db.MarkFeedFetched(context.Background(), feed.ID) + if err != nil { + log.Printf("Couldn't mark feed %s fetched: %v", feed.Name, err) + return + } + + feedData, err := fetchFeed(feed.Url) + if err != nil { + log.Printf("Couldn't collect feed %s: %v", feed.Name, err) + return + } + for _, item := range feedData.Channel.Item { + log.Println("Found post", item.Title) + } + log.Printf("Feed %s collected, %v posts found", feed.Name, len(feedData.Channel.Item)) +} + +type RSSFeed struct { + Channel struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Language string `xml:"language"` + Item []RSSItem `xml:"item"` + } `xml:"channel"` +} + +type RSSItem struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + PubDate string `xml:"pubDate"` +} + +func fetchFeed(feedURL string) (*RSSFeed, error) { + httpClient := http.Client{ + Timeout: 10 * time.Second, + } + resp, err := httpClient.Get(feedURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + dat, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var rssFeed RSSFeed + err = xml.Unmarshal(dat, &rssFeed) + if err != nil { + return nil, err + } + + return &rssFeed, nil +} diff --git a/project/9-scraper/src/sql/queries/feed_follows.sql b/project/9-scraper/src/sql/queries/feed_follows.sql new file mode 100644 index 0000000..ebef9a9 --- /dev/null +++ b/project/9-scraper/src/sql/queries/feed_follows.sql @@ -0,0 +1,13 @@ +-- name: GetFeedFollowsForUser :many +SELECT * FROM feed_follows WHERE user_id = $1; +-- + +-- name: CreateFeedFollow :one +INSERT INTO feed_follows (id, created_at, updated_at, user_id, feed_id) +VALUES ($1, $2, $3, $4, $5) +RETURNING *; +-- + +-- name: DeleteFeedFollow :exec +DELETE FROM feed_follows WHERE id = $1 and user_id = $2; +-- diff --git a/project/9-scraper/src/sql/queries/feeds.sql b/project/9-scraper/src/sql/queries/feeds.sql new file mode 100644 index 0000000..7d243fc --- /dev/null +++ b/project/9-scraper/src/sql/queries/feeds.sql @@ -0,0 +1,19 @@ +-- name: CreateFeed :one +INSERT INTO feeds (id, created_at, updated_at, name, url, user_id) +VALUES ($1, $2, $3, $4, $5, $6) +RETURNING *; + +-- name: GetFeeds :many +SELECT * FROM feeds; + +-- name: GetNextFeedsToFetch :many +SELECT * FROM feeds +ORDER BY last_fetched_at ASC NULLS FIRST +LIMIT $1; + +-- name: MarkFeedFetched :one +UPDATE feeds +SET last_fetched_at = NOW(), +updated_at = NOW() +WHERE id = $1 +RETURNING *; diff --git a/project/9-scraper/src/sql/queries/users.sql b/project/9-scraper/src/sql/queries/users.sql new file mode 100644 index 0000000..3f93c7e --- /dev/null +++ b/project/9-scraper/src/sql/queries/users.sql @@ -0,0 +1,13 @@ +-- name: CreateUser :one +INSERT INTO users (id, created_at, updated_at, name, api_key) +VALUES ( + $1, + $2, + $3, + $4, + encode(sha256(random()::text::bytea), 'hex') +) +RETURNING *; + +-- name: GetUserByAPIKey :one +SELECT * FROM users WHERE api_key = $1; diff --git a/project/9-scraper/src/sql/schema/001_users.sql b/project/9-scraper/src/sql/schema/001_users.sql new file mode 100644 index 0000000..513d77b --- /dev/null +++ b/project/9-scraper/src/sql/schema/001_users.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE users ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL +); + +-- +goose Down +DROP TABLE users; diff --git a/project/9-scraper/src/sql/schema/002_users_apikey.sql b/project/9-scraper/src/sql/schema/002_users_apikey.sql new file mode 100644 index 0000000..996ba8e --- /dev/null +++ b/project/9-scraper/src/sql/schema/002_users_apikey.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE users ADD COLUMN api_key VARCHAR(64) UNIQUE NOT NULL DEFAULT ( + encode(sha256(random()::text::bytea), 'hex') +); + +-- +goose Down +ALTER TABLE users DROP COLUMN api_key; diff --git a/project/9-scraper/src/sql/schema/003_feeds.sql b/project/9-scraper/src/sql/schema/003_feeds.sql new file mode 100644 index 0000000..8c9f831 --- /dev/null +++ b/project/9-scraper/src/sql/schema/003_feeds.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feeds ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE +); + +-- +goose Down +DROP TABLE feeds; diff --git a/project/9-scraper/src/sql/schema/004_feed_follows.sql b/project/9-scraper/src/sql/schema/004_feed_follows.sql new file mode 100644 index 0000000..f5e108b --- /dev/null +++ b/project/9-scraper/src/sql/schema/004_feed_follows.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE feed_follows ( + id UUID PRIMARY KEY, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + feed_id UUID NOT NULL REFERENCES feeds(id) ON DELETE CASCADE, + UNIQUE (user_id, feed_id) +); + +-- +goose Down +DROP TABLE feed_follows; diff --git a/project/9-scraper/src/sql/schema/005_feed_lastfetched.sql b/project/9-scraper/src/sql/schema/005_feed_lastfetched.sql new file mode 100644 index 0000000..6a38099 --- /dev/null +++ b/project/9-scraper/src/sql/schema/005_feed_lastfetched.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE feeds ADD COLUMN last_fetched_at TIMESTAMP; + +-- +goose Down +ALTER TABLE feeds DROP COLUMN last_fetched_at; diff --git a/project/9-scraper/src/sqlc.yaml b/project/9-scraper/src/sqlc.yaml new file mode 100644 index 0000000..08ef51e --- /dev/null +++ b/project/9-scraper/src/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/.gitignore b/project/9-scraper/src/vendor/github.com/go-chi/chi/.gitignore new file mode 100644 index 0000000..ba22c99 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/.gitignore @@ -0,0 +1,3 @@ +.idea +*.sw? +.vscode diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/CHANGELOG.md b/project/9-scraper/src/vendor/github.com/go-chi/chi/CHANGELOG.md new file mode 100644 index 0000000..7dd0791 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -0,0 +1,269 @@ +# Changelog + +## v1.5.4 (2021-02-27) + +- Undo prior retraction in v1.5.3 as we prepare for v5.0.0 release +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.3...v1.5.4 + + +## v1.5.3 (2021-02-21) + +- Update go.mod to go 1.16 with new retract directive marking all versions without prior go.mod support +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.2...v1.5.3 + + +## v1.5.2 (2021-02-10) + +- Reverting allocation optimization as a precaution as go test -race fails. +- Minor improvements, see history below +- History of changes: see https://github.com/go-chi/chi/compare/v1.5.1...v1.5.2 + + +## v1.5.1 (2020-12-06) + +- Performance improvement: removing 1 allocation by foregoing context.WithValue, thank you @bouk for + your contribution (https://github.com/go-chi/chi/pull/555). Note: new benchmarks posted in README. +- `middleware.CleanPath`: new middleware that clean's request path of double slashes +- deprecate & remove `chi.ServerBaseContext` in favour of stdlib `http.Server#BaseContext` +- plus other tiny improvements, see full commit history below +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.2...v1.5.1 + + +## v1.5.0 (2020-11-12) - now with go.mod support + +`chi` dates back to 2016 with it's original implementation as one of the first routers to adopt the newly introduced +context.Context api to the stdlib -- set out to design a router that is faster, more modular and simpler than anything +else out there -- while not introducing any custom handler types or dependencies. Today, `chi` still has zero dependencies, +and in many ways is future proofed from changes, given it's minimal nature. Between versions, chi's iterations have been very +incremental, with the architecture and api being the same today as it was originally designed in 2016. For this reason it +makes chi a pretty easy project to maintain, as well thanks to the many amazing community contributions over the years +to who all help make chi better (total of 86 contributors to date -- thanks all!). + +Chi has been an labour of love, art and engineering, with the goals to offer beautiful ergonomics, flexibility, performance +and simplicity when building HTTP services with Go. I've strived to keep the router very minimal in surface area / code size, +and always improving the code wherever possible -- and as of today the `chi` package is just 1082 lines of code (not counting +middlewares, which are all optional). As well, I don't have the exact metrics, but from my analysis and email exchanges from +companies and developers, chi is used by thousands of projects around the world -- thank you all as there is no better form of +joy for me than to have art I had started be helpful and enjoyed by others. And of course I use chi in all of my own projects too :) + +For me, the asthetics of chi's code and usage are very important. With the introduction of Go's module support +(which I'm a big fan of), chi's past versioning scheme choice to v2, v3 and v4 would mean I'd require the import path +of "github.com/go-chi/chi/v4", leading to the lengthy discussion at https://github.com/go-chi/chi/issues/462. +Haha, to some, you may be scratching your head why I've spent > 1 year stalling to adopt "/vXX" convention in the import +path -- which isn't horrible in general -- but for chi, I'm unable to accept it as I strive for perfection in it's API design, +aesthetics and simplicity. It just doesn't feel good to me given chi's simple nature -- I do not foresee a "v5" or "v6", +and upgrading between versions in the future will also be just incremental. + +I do understand versioning is a part of the API design as well, which is why the solution for a while has been to "do nothing", +as Go supports both old and new import paths with/out go.mod. However, now that Go module support has had time to iron out kinks and +is adopted everywhere, it's time for chi to get with the times. Luckily, I've discovered a path forward that will make me happy, +while also not breaking anyone's app who adopted a prior versioning from tags in v2/v3/v4. I've made an experimental release of +v1.5.0 with go.mod silently, and tested it with new and old projects, to ensure the developer experience is preserved, and it's +largely unnoticed. Fortunately, Go's toolchain will check the tags of a repo and consider the "latest" tag the one with go.mod. +However, you can still request a specific older tag such as v4.1.2, and everything will "just work". But new users can just +`go get github.com/go-chi/chi` or `go get github.com/go-chi/chi@latest` and they will get the latest version which contains +go.mod support, which is v1.5.0+. `chi` will not change very much over the years, just like it hasn't changed much from 4 years ago. +Therefore, we will stay on v1.x from here on, starting from v1.5.0. Any breaking changes will bump a "minor" release and +backwards-compatible improvements/fixes will bump a "tiny" release. + +For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). Brand new systems can run +`go get -u github.com/go-chi/chi` or `go get -u github.com/go-chi/chi@latest` to install chi, which will install v1.5.0+ +built with go.mod support. + +My apologies to the developers who will disagree with the decisions above, but, hope you'll try it and see it's a very +minor request which is backwards compatible and won't break your existing installations. + +Cheers all, happy coding! + + +--- + + +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + +## v4.0.0 (2019-01-10) + +- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 +- router: respond with 404 on router with no routes (#362) +- router: additional check to ensure wildcard is at the end of a url pattern (#333) +- middleware: deprecate use of http.CloseNotifier (#347) +- middleware: fix RedirectSlashes to include query params on redirect (#334) +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 + + +## v3.3.4 (2019-01-07) + +- Minor middleware improvements. No changes to core library/router. Moving v3 into its +- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 +- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 + + +## v3.3.3 (2018-08-27) + +- Minor release +- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 + + +## v3.3.2 (2017-12-22) + +- Support to route trailing slashes on mounted sub-routers (#281) +- middleware: new `ContentCharset` to check matching charsets. Thank you + @csucu for your community contribution! + + +## v3.3.1 (2017-11-20) + +- middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types +- middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value +- Minor bug fixes + + +## v3.3.0 (2017-10-10) + +- New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage +- Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function + + +## v3.2.1 (2017-08-31) + +- Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface + and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path +- Add new `RouteMethod` to `*Context` +- Add new `Routes` pointer to `*Context` +- Add new `middleware.GetHead` to route missing HEAD requests to GET handler +- Updated benchmarks (see README) + + +## v3.1.5 (2017-08-02) + +- Setup golint and go vet for the project +- As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` + to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` + + +## v3.1.0 (2017-07-10) + +- Fix a few minor issues after v3 release +- Move `docgen` sub-pkg to https://github.com/go-chi/docgen +- Move `render` sub-pkg to https://github.com/go-chi/render +- Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime + suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in + https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. + + +## v3.0.0 (2017-06-21) + +- Major update to chi library with many exciting updates, but also some *breaking changes* +- URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as + `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the + same router +- Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: + `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` +- Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as + `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like + in `_examples/custom-handler` +- Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their + own using file handler with the stdlib, see `_examples/fileserver` for an example +- Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` +- Moved the chi project to its own organization, to allow chi-related community packages to + be easily discovered and supported, at: https://github.com/go-chi +- *NOTE:* please update your import paths to `"github.com/go-chi/chi"` +- *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 + + +## v2.1.0 (2017-03-30) + +- Minor improvements and update to the chi core library +- Introduced a brand new `chi/render` sub-package to complete the story of building + APIs to offer a pattern for managing well-defined request / response payloads. Please + check out the updated `_examples/rest` example for how it works. +- Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface + + +## v2.0.0 (2017-01-06) + +- After many months of v2 being in an RC state with many companies and users running it in + production, the inclusion of some improvements to the middlewares, we are very pleased to + announce v2.0.0 of chi. + + +## v2.0.0-rc1 (2016-07-26) + +- Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular + community `"net/context"` package has been included in the standard library as `"context"` and + utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other + request-scoped values. We're very excited about the new context addition and are proud to + introduce chi v2, a minimal and powerful routing package for building large HTTP services, + with zero external dependencies. Chi focuses on idiomatic design and encourages the use of + stdlib HTTP handlers and middlwares. +- chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` +- chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` +- chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, + which provides direct access to URL routing parameters, the routing path and the matching + routing patterns. +- Users upgrading from chi v1 to v2, need to: + 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to + the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` + 2. Use `chi.URLParam(r *http.Request, paramKey string) string` + or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value + + +## v1.0.0 (2016-07-01) + +- Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. + + +## v0.9.0 (2016-03-31) + +- Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) +- BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters + has changed to: `chi.URLParam(ctx, "id")` diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md b/project/9-scraper/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md new file mode 100644 index 0000000..c0ac2df --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +## Prerequisites + +1. [Install Go][go-install]. +2. Download the sources and switch the working directory: + + ```bash + go get -u -d github.com/go-chi/chi + cd $GOPATH/src/github.com/go-chi/chi + ``` + +## Submitting a Pull Request + +A typical workflow is: + +1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] +2. [Create a topic branch.][branch] +3. Add tests for your change. +4. Run `go test`. If your tests pass, return to the step 3. +5. Implement the change and ensure the steps from the previous step pass. +6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. +7. [Add, commit and push your changes.][git-help] +8. [Submit a pull request.][pull-req] + +[go-install]: https://golang.org/doc/install +[go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html +[fork]: https://help.github.com/articles/fork-a-repo +[branch]: http://learn.github.com/p/branching.html +[git-help]: https://guides.github.com +[pull-req]: https://help.github.com/articles/using-pull-requests diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/LICENSE b/project/9-scraper/src/vendor/github.com/go-chi/chi/LICENSE new file mode 100644 index 0000000..d99f02f --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/Makefile b/project/9-scraper/src/vendor/github.com/go-chi/chi/Makefile new file mode 100644 index 0000000..b96c92d --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/Makefile @@ -0,0 +1,14 @@ +all: + @echo "**********************************************************" + @echo "** chi build tool **" + @echo "**********************************************************" + + +test: + go clean -testcache && $(MAKE) test-router && $(MAKE) test-middleware + +test-router: + go test -race -v . + +test-middleware: + go test -race -v ./middleware diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/README.md b/project/9-scraper/src/vendor/github.com/go-chi/chi/README.md new file mode 100644 index 0000000..1b96d36 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/README.md @@ -0,0 +1,511 @@ +# chi + + +[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] + +`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's +especially good at helping you write large REST API services that are kept maintainable as your +project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to +handle signaling, cancelation and request-scoped values across a handler chain. + +The focus of the project has been to seek out an elegant and comfortable design for writing +REST API servers, written during the development of the Pressly API service that powers our +public API service, which in turn powers all of our client-side applications. + +The key considerations of chi's design are: project structure, maintainability, standard http +handlers (stdlib-only), developer productivity, and deconstructing a large system into many small +parts. The core router `github.com/go-chi/chi` is quite small (less than 1000 LOC), but we've also +included some useful/optional subpackages: [middleware](/middleware), [render](https://github.com/go-chi/render) +and [docgen](https://github.com/go-chi/docgen). We hope you enjoy it too! + +## Install + +`go get -u github.com/go-chi/chi` + + +## Features + +* **Lightweight** - cloc'd in ~1000 LOC for the chi router +* **Fast** - yes, see [benchmarks](#benchmarks) +* **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` +* **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and sub-router mounting +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts +* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) +* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown +* **Go.mod support** - v1.x of chi (starting from v1.5.0), now has go.mod support (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* **No external dependencies** - plain ol' Go stdlib + net/http + + +## Examples + +See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples. + + +**As easy as:** + +```go +package main + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + http.ListenAndServe(":3000", r) +} +``` + +**REST Preview:** + +Here is a little preview of how routing looks like with chi. Also take a look at the generated routing docs +in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in +Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)). + +I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed +above, they will show you all the features of chi and serve as a good form of documentation. + +```go +import ( + //... + "context" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" +) + +func main() { + r := chi.NewRouter() + + // A good base middleware stack + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Set a timeout value on the request context (ctx), that will signal + // through ctx.Done() that the request has timed out and further + // processing should be stopped. + r.Use(middleware.Timeout(60 * time.Second)) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("hi")) + }) + + // RESTy routes for "articles" resource + r.Route("/articles", func(r chi.Router) { + r.With(paginate).Get("/", listArticles) // GET /articles + r.With(paginate).Get("/{month}-{day}-{year}", listArticlesByDate) // GET /articles/01-16-2017 + + r.Post("/", createArticle) // POST /articles + r.Get("/search", searchArticles) // GET /articles/search + + // Regexp url parameters: + r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug) // GET /articles/home-is-toronto + + // Subrouters: + r.Route("/{articleID}", func(r chi.Router) { + r.Use(ArticleCtx) + r.Get("/", getArticle) // GET /articles/123 + r.Put("/", updateArticle) // PUT /articles/123 + r.Delete("/", deleteArticle) // DELETE /articles/123 + }) + }) + + // Mount the admin sub-router + r.Mount("/admin", adminRouter()) + + http.ListenAndServe(":3333", r) +} + +func ArticleCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + articleID := chi.URLParam(r, "articleID") + article, err := dbGetArticle(articleID) + if err != nil { + http.Error(w, http.StatusText(404), 404) + return + } + ctx := context.WithValue(r.Context(), "article", article) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func getArticle(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + article, ok := ctx.Value("article").(*Article) + if !ok { + http.Error(w, http.StatusText(422), 422) + return + } + w.Write([]byte(fmt.Sprintf("title:%s", article.Title))) +} + +// A completely separate router for administrator routes +func adminRouter() http.Handler { + r := chi.NewRouter() + r.Use(AdminOnly) + r.Get("/", adminIndex) + r.Get("/accounts", adminListAccounts) + return r +} + +func AdminOnly(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + perm, ok := ctx.Value("acl.permission").(YourPermissionType) + if !ok || !perm.IsAdmin() { + http.Error(w, http.StatusText(403), 403) + return + } + next.ServeHTTP(w, r) + }) +} +``` + + +## Router interface + +chi's router is based on a kind of [Patricia Radix trie](https://en.wikipedia.org/wiki/Radix_tree). +The router is fully compatible with `net/http`. + +Built on top of the tree is the `Router` interface: + +```go +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the github.com/go-chi/docgen package to generate documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} +``` + +Each routing method accepts a URL `pattern` and chain of `handlers`. The URL pattern +supports named params (ie. `/users/{userID}`) and wildcards (ie. `/admin/*`). URL parameters +can be fetched at runtime by calling `chi.URLParam(r, "userID")` for named parameters +and `chi.URLParam(r, "*")` for a wildcard parameter. + + +### Middleware handlers + +chi's middlewares are just stdlib net/http middleware handlers. There is nothing special +about them, which means the router and all the tooling is designed to be compatible and +friendly with any middleware in the community. This offers much better extensibility and reuse +of packages and is at the heart of chi's purpose. + +Here is an example of a standard net/http middleware where we assign a context key `"user"` +the value of `"123"`. This middleware sets a hypothetical user identifier on the request +context and calls the next handler in the chain. + +```go +// HTTP middleware setting a value on the request context +func MyMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // create new context from `r` request context, and assign key `"user"` + // to value of `"123"` + ctx := context.WithValue(r.Context(), "user", "123") + + // call the next handler in the chain, passing the response writer and + // the updated request object with the new context value. + // + // note: context.Context values are nested, so any previously set + // values will be accessible as well, and the new `"user"` key + // will be accessible from this point forward. + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + + +### Request handlers + +chi uses standard net/http request handlers. This little snippet is an example of a http.Handler +func that reads a user identifier from the request context - hypothetically, identifying +the user sending an authenticated request, validated+set by a previous middleware handler. + +```go +// HTTP handler accessing data from the request context. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // here we read from the request context and fetch out `"user"` key set in + // the MyMiddleware example above. + user := r.Context().Value("user").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %s", user))) +} +``` + + +### URL parameters + +chi's router parses and stores URL parameters right onto the request context. Here is +an example of how to access URL params in your net/http handlers. And of course, middlewares +are able to access the same information. + +```go +// HTTP handler accessing the url routing parameters. +func MyRequestHandler(w http.ResponseWriter, r *http.Request) { + // fetch the url parameter `"userID"` from the request of a matching + // routing pattern. An example routing pattern could be: /users/{userID} + userID := chi.URLParam(r, "userID") + + // fetch `"key"` from the request context + ctx := r.Context() + key := ctx.Value("key").(string) + + // respond to the client + w.Write([]byte(fmt.Sprintf("hi %v, %v", userID, key))) +} +``` + + +## Middlewares + +chi comes equipped with an optional `middleware` package, providing a suite of standard +`net/http` middlewares. Please note, any middleware in the ecosystem that is also compatible +with `net/http` can be used with chi's mux. + +### Core middlewares + +---------------------------------------------------------------------------------------------------- +| chi/middleware Handler | description | +| :--------------------- | :---------------------------------------------------------------------- | +| [AllowContentEncoding] | Enforces a whitelist of request Content-Encoding headers | +| [AllowContentType] | Explicit whitelist of accepted request Content-Types | +| [BasicAuth] | Basic HTTP authentication | +| [Compress] | Gzip compression for clients that accept compressed responses | +| [ContentCharset] | Ensure charset for Content-Type request headers | +| [CleanPath] | Clean double slashes from request path | +| [GetHead] | Automatically route undefined HEAD requests to GET handlers | +| [Heartbeat] | Monitoring endpoint to check the servers pulse | +| [Logger] | Logs the start and end of each request with the elapsed processing time | +| [NoCache] | Sets response headers to prevent clients from caching | +| [Profiler] | Easily attach net/http/pprof to your routers | +| [RealIP] | Sets a http.Request's RemoteAddr to either X-Forwarded-For or X-Real-IP | +| [Recoverer] | Gracefully absorb panics and prints the stack trace | +| [RequestID] | Injects a request ID into the context of each request | +| [RedirectSlashes] | Redirect slashes on routing paths | +| [RouteHeaders] | Route handling for request headers | +| [SetHeader] | Short-hand middleware to set a response header key/value | +| [StripSlashes] | Strip slashes on routing paths | +| [Throttle] | Puts a ceiling on the number of concurrent requests | +| [Timeout] | Signals to the request context when the timeout deadline is reached | +| [URLFormat] | Parse extension from url and put it on request context | +| [WithValue] | Short-hand middleware to set a key/value on the request context | +---------------------------------------------------------------------------------------------------- + +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter + +### Extra middlewares & packages + +Please see https://github.com/go-chi for additional packages. + +-------------------------------------------------------------------------------------------------------------------- +| package | description | +|:---------------------------------------------------|:------------------------------------------------------------- +| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) | +| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | +| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | +| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | +-------------------------------------------------------------------------------------------------------------------- + + +## context? + +`context` is a tiny pkg that provides simple interface to signal context across call stacks +and goroutines. It was originally written by [Sameer Ajmani](https://github.com/Sajmani) +and is available in stdlib since go1.7. + +Learn more at https://blog.golang.org/context + +and.. +* Docs: https://golang.org/pkg/context +* Source: https://github.com/golang/go/tree/master/src/context + + +## Benchmarks + +The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark + +Results as of Nov 29, 2020 with Go 1.15.5 on Linux AMD 3950x + +```shell +BenchmarkChi_Param 3075895 384 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param5 2116603 566 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Param20 964117 1227 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParamWrite 2863413 420 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubStatic 3045488 395 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubParam 2204115 540 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GithubAll 10000 113811 ns/op 81203 B/op 406 allocs/op +BenchmarkChi_GPlusStatic 3337485 359 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusParam 2825853 423 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlus2Params 2471697 483 ns/op 400 B/op 2 allocs/op +BenchmarkChi_GPlusAll 194220 5950 ns/op 5200 B/op 26 allocs/op +BenchmarkChi_ParseStatic 3365324 356 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseParam 2976614 404 ns/op 400 B/op 2 allocs/op +BenchmarkChi_Parse2Params 2638084 439 ns/op 400 B/op 2 allocs/op +BenchmarkChi_ParseAll 109567 11295 ns/op 10400 B/op 52 allocs/op +BenchmarkChi_StaticAll 16846 71308 ns/op 62802 B/op 314 allocs/op +``` + +Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc + +NOTE: the allocs in the benchmark above are from the calls to http.Request's +`WithContext(context.Context)` method that clones the http.Request, sets the `Context()` +on the duplicated (alloc'd) request and returns it the new request object. This is just +how setting context on a request in Go works. + + +## Go module support & note on chi's versioning + +* Go.mod support means we reset our versioning starting from v1.5 (see [CHANGELOG](https://github.com/go-chi/chi/blob/master/CHANGELOG.md#v150-2020-11-12---now-with-gomod-support)) +* All older tags are preserved, are backwards-compatible and will "just work" as they +* Brand new systems can run `go get -u github.com/go-chi/chi` as normal, or `go get -u github.com/go-chi/chi@latest` +to install chi, which will install v1.x+ built with go.mod support, starting from v1.5.0. +* For existing projects who want to upgrade to the latest go.mod version, run: `go get -u github.com/go-chi/chi@v1.5.0`, +which will get you on the go.mod version line (as Go's mod cache may still remember v4.x). +* Any breaking changes will bump a "minor" release and backwards-compatible improvements/fixes will bump a "tiny" release. + + +## Credits + +* Carl Jackson for https://github.com/zenazn/goji + * Parts of chi's thinking comes from goji, and chi's middleware package + sources from goji. +* Armon Dadgar for https://github.com/armon/go-radix +* Contributions: [@VojtechVitek](https://github.com/VojtechVitek) + +We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! + + +## Beyond REST + +chi is just a http router that lets you decompose request handling into many smaller layers. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub + + +## License + +Copyright (c) 2015-present [Peter Kieltyka](https://github.com/pkieltyka) + +Licensed under [MIT License](./LICENSE) + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/go-chi/chi?status.svg +[Travis]: https://travis-ci.org/go-chi/chi +[Travis Widget]: https://travis-ci.org/go-chi/chi.svg?branch=master diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/chain.go b/project/9-scraper/src/vendor/github.com/go-chi/chi/chain.go new file mode 100644 index 0000000..88e6846 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/chain.go @@ -0,0 +1,49 @@ +package chi + +import "net/http" + +// Chain returns a Middlewares type from a slice of middleware handlers. +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// HandlerFunc builds and returns a http.Handler from the chain of middlewares, +// with `h http.Handler` as the final handler. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return &ChainHandler{mws, h, chain(mws, h)} +} + +// ChainHandler is a http.Handler with support for handler composition and +// execution. +type ChainHandler struct { + Middlewares Middlewares + Endpoint http.Handler + chain http.Handler +} + +func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c.chain.ServeHTTP(w, r) +} + +// chain builds a http.Handler composed of an inline middleware stack and endpoint +// handler in the order they are passed. +func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { + // Return ahead of time if there aren't any middlewares for the chain + if len(middlewares) == 0 { + return endpoint + } + + // Wrap the end handler with the middleware chain + h := middlewares[len(middlewares)-1](endpoint) + for i := len(middlewares) - 2; i >= 0; i-- { + h = middlewares[i](h) + } + + return h +} diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/chi.go b/project/9-scraper/src/vendor/github.com/go-chi/chi/chi.go new file mode 100644 index 0000000..b7063dc --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/chi.go @@ -0,0 +1,134 @@ +// +// Package chi is a small, idiomatic and composable router for building HTTP services. +// +// chi requires Go 1.10 or newer. +// +// Example: +// package main +// +// import ( +// "net/http" +// +// "github.com/go-chi/chi" +// "github.com/go-chi/chi/middleware" +// ) +// +// func main() { +// r := chi.NewRouter() +// r.Use(middleware.Logger) +// r.Use(middleware.Recoverer) +// +// r.Get("/", func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("root.")) +// }) +// +// http.ListenAndServe(":3333", r) +// } +// +// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// +// URL patterns allow for easy matching of path components in HTTP +// requests. The matching components can then be accessed using +// chi.URLParam(). All patterns must begin with a slash. +// +// A simple named placeholder {name} matches any sequence of characters +// up to the next / or the end of the URL. Trailing slashes on paths must +// be handled explicitly. +// +// A placeholder with a name followed by a colon allows a regular +// expression match, for example {number:\\d+}. The regular expression +// syntax is Go's normal regexp RE2 syntax, except that regular expressions +// including { or } are not supported, and / will never be +// matched. An anonymous regexp pattern is allowed, using an empty string +// before the colon in the placeholder, such as {:\\d+} +// +// The special placeholder of asterisk matches the rest of the requested +// URL. Any trailing characters in the pattern are ignored. This is the only +// placeholder which will match / characters. +// +// Examples: +// "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" +// "/user/{name}/info" matches "/user/jsmith/info" +// "/page/*" matches "/page/intro/latest" +// "/page/*/index" also matches "/page/intro/latest" +// "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" +// +package chi + +import "net/http" + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} + +// Router consisting of the core routing methods used by chi's Mux, +// using only the standard net/http. +type Router interface { + http.Handler + Routes + + // Use appends one or more middlewares onto the Router stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group adds a new inline-Router along the current routing + // path, with a fresh middleware stack for the inline-Router. + Group(fn func(r Router)) Router + + // Route mounts a sub-Router along a `pattern`` string. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, h http.Handler) + + // Handle and HandleFunc adds routes for `pattern` that matches + // all HTTP methods. + Handle(pattern string, h http.Handler) + HandleFunc(pattern string, h http.HandlerFunc) + + // Method and MethodFunc adds routes for `pattern` that matches + // the `method` HTTP method. + Method(method, pattern string, h http.Handler) + MethodFunc(method, pattern string, h http.HandlerFunc) + + // HTTP-method routing along `pattern` + Connect(pattern string, h http.HandlerFunc) + Delete(pattern string, h http.HandlerFunc) + Get(pattern string, h http.HandlerFunc) + Head(pattern string, h http.HandlerFunc) + Options(pattern string, h http.HandlerFunc) + Patch(pattern string, h http.HandlerFunc) + Post(pattern string, h http.HandlerFunc) + Put(pattern string, h http.HandlerFunc) + Trace(pattern string, h http.HandlerFunc) + + // NotFound defines a handler to respond whenever a route could + // not be found. + NotFound(h http.HandlerFunc) + + // MethodNotAllowed defines a handler to respond whenever a method is + // not allowed. + MethodNotAllowed(h http.HandlerFunc) +} + +// Routes interface adds two methods for router traversal, which is also +// used by the `docgen` subpackage to generation documentation for Routers. +type Routes interface { + // Routes returns the routing tree in an easily traversable structure. + Routes() []Route + + // Middlewares returns the list of middlewares in use by the router. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches + // the method/path - similar to routing a http request, but without + // executing the handler thereafter. + Match(rctx *Context, method, path string) bool +} + +// Middlewares type is a slice of standard middleware handlers with methods +// to compose middleware chains and http.Handler's. +type Middlewares []func(http.Handler) http.Handler diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/context.go b/project/9-scraper/src/vendor/github.com/go-chi/chi/context.go new file mode 100644 index 0000000..8c97f21 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/context.go @@ -0,0 +1,157 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it cant be tampered. + routeParams RouteParams + + // methodNotAllowed hint + methodNotAllowed bool + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why its advised to only use this value after calling the next handler. +// +// For example, +// +// func Instrument(next http.Handler) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// next.ServeHTTP(w, r) +// routePattern := chi.RouteContext(r.Context()).RoutePattern() +// measure(w, r, routePattern) +// }) +// } +func (x *Context) RoutePattern() string { + routePattern := strings.Join(x.RoutePatterns, "") + return replaceWildcards(routePattern) +} + +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + } + + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/mux.go b/project/9-scraper/src/vendor/github.com/go-chi/chi/mux.go new file mode 100644 index 0000000..146643b --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/mux.go @@ -0,0 +1,479 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +// +// Mux is designed to be fast, minimal and offer a powerful API for building +// modular and composable HTTP services with a large set of handlers. It's +// particularly useful for writing large REST API services that break a handler +// into many smaller parts composed of middlewares and end handlers. +type Mux struct { + // The radix trie router + tree *node + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool + parent *Mux + + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +// +// The middleware stack for any Mux will execute before searching for a matching +// route to a specific handler, which provides opportunity to respond early, +// change the course of the request execution, or set request-scoped values for +// the next http.Handler. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mALL, pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches a OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(h) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + h := Chain(mx.middlewares...).HandlerFunc(handlerFn).ServeHTTP + if mx.inline && mx.parent != nil { + m = mx.parent + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = h + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(h) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a fresh middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. See _examples/. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With().(*Mux) + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux with a fresh middleware stack and mounts it +// along the `pattern` as a subrouter. Effectively, this is a short-hand +// call to Mount. See _examples/. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. See _examples/. +// +// Note that Mount() simply sets a wildcard along the `pattern` that will continue +// routing at the `handler`, which in most cases is another chi.Router. As a result, +// if you define two Mount() routes on the exact same pattern the mount will panic. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Routes) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +// It's similar to routing a http request, but without executing the handler +// thereafter. +// +// Note: the *Context state is updated during execution, so manage +// the state carefully or make a NewRouteContext(). +func (mx *Mux) Match(rctx *Context, method, path string) bool { + m, ok := methodMap[method] + if !ok { + return false + } + + node, _, h := mx.tree.FindRoute(rctx, m, path) + + if node != nil && node.subroutes != nil { + rctx.RoutePath = mx.nextRoutePath(rctx) + return node.subroutes.Match(rctx, method, rctx.RoutePath) + } + + return h != nil +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler() http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// Recursively update data on child routers. +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + for _, r := range mx.tree.routes() { + subMux, ok := r.SubRoutes.(*Mux) + if !ok { + continue + } + fn(subMux) + } +} + +// updateRouteHandler builds the single mux handler that is a chain of the middleware +// stack, as defined by calls to Use(), and the tree router (Mux) itself. After this +// point, no other middlewares can be registered on this Mux's stack. But you can still +// compose additional middlewares via Group()'s or using a chained middleware handler. +func (mx *Mux) updateRouteHandler() { + mx.handler = chain(mx.middlewares, http.HandlerFunc(mx.routeHTTP)) +} + +// methodNotAllowedHandler is a helper function to respond with a 405, +// method not allowed. +func methodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(405) + w.Write(nil) +} diff --git a/project/9-scraper/src/vendor/github.com/go-chi/chi/tree.go b/project/9-scraper/src/vendor/github.com/go-chi/chi/tree.go new file mode 100644 index 0000000..8057c52 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/chi/tree.go @@ -0,0 +1,866 @@ +package chi + +// Radix tree implementation below is a based on the original work by +// Armon Dadgar in https://github.com/armon/go-radix/blob/master/radix.go +// (MIT licensed). It's been heavily modified for use as a HTTP routing tree. + +import ( + "fmt" + "net/http" + "regexp" + "sort" + "strconv" + "strings" +) + +type methodTyp int + +const ( + mSTUB methodTyp = 1 << iota + mCONNECT + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE +) + +var mALL = mCONNECT | mDELETE | mGET | mHEAD | + mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + +var methodMap = map[string]methodTyp{ + http.MethodConnect: mCONNECT, + http.MethodDelete: mDELETE, + http.MethodGet: mGET, + http.MethodHead: mHEAD, + http.MethodOptions: mOPTIONS, + http.MethodPatch: mPATCH, + http.MethodPost: mPOST, + http.MethodPut: mPUT, + http.MethodTrace: mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available +// via Router#Method and Router#MethodFunc +func RegisterMethod(method string) { + if method == "" { + return + } + method = strings.ToUpper(method) + if _, ok := methodMap[method]; ok { + return + } + n := len(methodMap) + if n > strconv.IntSize-2 { + panic(fmt.Sprintf("chi: max number of methods reached (%d)", strconv.IntSize)) + } + mt := methodTyp(2 << n) + methodMap[method] = mt + mALL |= mt +} + +type nodeTyp uint8 + +const ( + ntStatic nodeTyp = iota // /home + ntRegexp // /{id:[0-9]+} + ntParam // /{user} + ntCatchAll // /api/v1/* +) + +type node struct { + // node type: static, regexp, param, catchAll + typ nodeTyp + + // first byte of the prefix + label byte + + // first byte of the child prefix + tail byte + + // prefix is the common prefix we ignore + prefix string + + // regexp matcher for regexp nodes + rex *regexp.Regexp + + // HTTP handler endpoints on the leaf node + endpoints endpoints + + // subroutes on the leaf node + subroutes Routes + + // child nodes should be stored in-order for iteration, + // in groups of the node type. + children [ntCatchAll + 1]nodes +} + +// endpoints is a mapping of http method constants to handlers +// for a given route. +type endpoints map[methodTyp]*endpoint + +type endpoint struct { + // endpoint handler + handler http.Handler + + // pattern is the routing pattern for handler nodes + pattern string + + // parameter keys recorded on handler nodes + paramKeys []string +} + +func (s endpoints) Value(method methodTyp) *endpoint { + mh, ok := s[method] + if !ok { + mh = &endpoint{} + s[method] = mh + } + return mh +} + +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + var parent *node + search := pattern + + for { + // Handle key exhaustion + if len(search) == 0 { + // Insert or update the node's leaf handler + n.setEndpoint(method, handler, pattern) + return n + } + + // We're going to be searching for a wild node next, + // in this case, we need to get the tail + var label = search[0] + var segTail byte + var segEndIdx int + var segTyp nodeTyp + var segRexpat string + if label == '{' || label == '*' { + segTyp, _, segRexpat, segTail, _, segEndIdx = patNextSegment(search) + } + + var prefix string + if segTyp == ntRegexp { + prefix = segRexpat + } + + // Look for the edge to attach to + parent = n + n = n.getEdge(segTyp, label, segTail, prefix) + + // No edge, create one + if n == nil { + child := &node{label: label, tail: segTail, prefix: search} + hn := parent.addChild(child, search) + hn.setEndpoint(method, handler, pattern) + + return hn + } + + // Found an edge to match the pattern + + if n.typ > ntStatic { + // We found a param node, trim the param from the search path and continue. + // This param/wild pattern segment would already be on the tree from a previous + // call to addChild when creating a new node. + search = search[segEndIdx:] + continue + } + + // Static nodes fall below here. + // Determine longest prefix of the search key on match. + commonPrefix := longestPrefix(search, n.prefix) + if commonPrefix == len(n.prefix) { + // the common prefix is as long as the current node's prefix we're attempting to insert. + // keep the search going. + search = search[commonPrefix:] + continue + } + + // Split the node + child := &node{ + typ: ntStatic, + prefix: search[:commonPrefix], + } + parent.replaceChild(search[0], segTail, child) + + // Restore the existing node + n.label = n.prefix[commonPrefix] + n.prefix = n.prefix[commonPrefix:] + child.addChild(n, n.prefix) + + // If the new key is a subset, set the method/handler on this node and finish. + search = search[commonPrefix:] + if len(search) == 0 { + child.setEndpoint(method, handler, pattern) + return child + } + + // Create a new edge for the node + subchild := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn := child.addChild(subchild, search) + hn.setEndpoint(method, handler, pattern) + return hn + } +} + +// addChild appends the new `child` node to the tree using the `pattern` as the trie key. +// For a URL router like chi's, we split the static, param, regexp and wildcard segments +// into different nodes. In addition, addChild will recursively call itself until every +// pattern segment is added to the url pattern tree as individual nodes, depending on type. +func (n *node) addChild(child *node, prefix string) *node { + search := prefix + + // handler leaf node added to the tree is the child. + // this may be overridden later down the flow + hn := child + + // Parse next segment + segTyp, _, segRexpat, segTail, segStartIdx, segEndIdx := patNextSegment(search) + + // Add child depending on next up segment + switch segTyp { + + case ntStatic: + // Search prefix is all static (that is, has no params in path) + // noop + + default: + // Search prefix contains a param, regexp or wildcard + + if segTyp == ntRegexp { + rex, err := regexp.Compile(segRexpat) + if err != nil { + panic(fmt.Sprintf("chi: invalid regexp pattern '%s' in route param", segRexpat)) + } + child.prefix = segRexpat + child.rex = rex + } + + if segStartIdx == 0 { + // Route starts with a param + child.typ = segTyp + + if segTyp == ntCatchAll { + segStartIdx = -1 + } else { + segStartIdx = segEndIdx + } + if segStartIdx < 0 { + segStartIdx = len(search) + } + child.tail = segTail // for params, we set the tail + + if segStartIdx != len(search) { + // add static edge for the remaining part, split the end. + // its not possible to have adjacent param nodes, so its certainly + // going to be a static node next. + + search = search[segStartIdx:] // advance search position + + nn := &node{ + typ: ntStatic, + label: search[0], + prefix: search, + } + hn = child.addChild(nn, search) + } + + } else if segStartIdx > 0 { + // Route has some param + + // starts with a static segment + child.typ = ntStatic + child.prefix = search[:segStartIdx] + child.rex = nil + + // add the param edge node + search = search[segStartIdx:] + + nn := &node{ + typ: segTyp, + label: search[0], + tail: segTail, + } + hn = child.addChild(nn, search) + + } + } + + n.children[child.typ] = append(n.children[child.typ], child) + n.children[child.typ].Sort() + return hn +} + +func (n *node) replaceChild(label, tail byte, child *node) { + for i := 0; i < len(n.children[child.typ]); i++ { + if n.children[child.typ][i].label == label && n.children[child.typ][i].tail == tail { + n.children[child.typ][i] = child + n.children[child.typ][i].label = label + n.children[child.typ][i].tail = tail + return + } + } + panic("chi: replacing missing child") +} + +func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { + nds := n.children[ntyp] + for i := 0; i < len(nds); i++ { + if nds[i].label == label && nds[i].tail == tail { + if ntyp == ntRegexp && nds[i].prefix != prefix { + continue + } + return nds[i] + } + } + return nil +} + +func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { + // Set the handler for the method type on the node + if n.endpoints == nil { + n.endpoints = make(endpoints) + } + + paramKeys := patParamKeys(pattern) + + if method&mSTUB == mSTUB { + n.endpoints.Value(mSTUB).handler = handler + } + if method&mALL == mALL { + h := n.endpoints.Value(mALL) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + for _, m := range methodMap { + h := n.endpoints.Value(m) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } + } else { + h := n.endpoints.Value(method) + h.handler = handler + h.pattern = pattern + h.paramKeys = paramKeys + } +} + +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoints, http.Handler) { + // Reset the context routing pattern and params + rctx.routePattern = "" + rctx.routeParams.Keys = rctx.routeParams.Keys[:0] + rctx.routeParams.Values = rctx.routeParams.Values[:0] + + // Find the routing handlers for the path + rn := n.findRoute(rctx, method, path) + if rn == nil { + return nil, nil, nil + } + + // Record the routing params in the request lifecycle + rctx.URLParams.Keys = append(rctx.URLParams.Keys, rctx.routeParams.Keys...) + rctx.URLParams.Values = append(rctx.URLParams.Values, rctx.routeParams.Values...) + + // Record the routing pattern in the request lifecycle + if rn.endpoints[method].pattern != "" { + rctx.routePattern = rn.endpoints[method].pattern + rctx.RoutePatterns = append(rctx.RoutePatterns, rctx.routePattern) + } + + return rn, rn.endpoints, rn.endpoints[method].handler +} + +// Recursive edge traversal by checking all nodeTyp groups along the way. +// It's like searching through a multi-dimensional radix trie. +func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { + nn := n + search := path + + for t, nds := range nn.children { + ntyp := nodeTyp(t) + if len(nds) == 0 { + continue + } + + var xn *node + xsearch := search + + var label byte + if search != "" { + label = search[0] + } + + switch ntyp { + case ntStatic: + xn = nds.findEdge(label) + if xn == nil || !strings.HasPrefix(xsearch, xn.prefix) { + continue + } + xsearch = xsearch[len(xn.prefix):] + + case ntParam, ntRegexp: + // short-circuit and return no matching route for empty param values + if xsearch == "" { + continue + } + + // serially loop through each node grouped by the tail delimiter + for idx := 0; idx < len(nds); idx++ { + xn = nds[idx] + + // label for param nodes is the delimiter byte + p := strings.IndexByte(xsearch, xn.tail) + + if p < 0 { + if xn.tail == '/' { + p = len(xsearch) + } else { + continue + } + } else if ntyp == ntRegexp && p == 0 { + continue + } + + if ntyp == ntRegexp && xn.rex != nil { + if !xn.rex.MatchString(xsearch[:p]) { + continue + } + } else if strings.IndexByte(xsearch[:p], '/') != -1 { + // avoid a match across path segments + continue + } + + prevlen := len(rctx.routeParams.Values) + rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) + xsearch = xsearch[p:] + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search + } + + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + + default: + // catch-all nodes + rctx.routeParams.Values = append(rctx.routeParams.Values, search) + xn = nds[0] + xsearch = "" + } + + if xn == nil { + continue + } + + // did we find it yet? + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node.. + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // Did not find final handler, let's remove the param here if it was set + if xn.typ > ntStatic { + if len(rctx.routeParams.Values) > 0 { + rctx.routeParams.Values = rctx.routeParams.Values[:len(rctx.routeParams.Values)-1] + } + } + + } + + return nil +} + +func (n *node) findEdge(ntyp nodeTyp, label byte) *node { + nds := n.children[ntyp] + num := len(nds) + idx := 0 + + switch ntyp { + case ntStatic, ntParam, ntRegexp: + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > nds[idx].label { + i = idx + 1 + } else if label < nds[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if nds[idx].label != label { + return nil + } + return nds[idx] + + default: // catch all + return nds[idx] + } +} + +func (n *node) isLeaf() bool { + return n.endpoints != nil +} + +func (n *node) findPattern(pattern string) bool { + nn := n + for _, nds := range nn.children { + if len(nds) == 0 { + continue + } + + n = nn.findEdge(nds[0].typ, pattern[0]) + if n == nil { + continue + } + + var idx int + var xpattern string + + switch n.typ { + case ntStatic: + idx = longestPrefix(pattern, n.prefix) + if idx < len(n.prefix) { + continue + } + + case ntParam, ntRegexp: + idx = strings.IndexByte(pattern, '}') + 1 + + case ntCatchAll: + idx = longestPrefix(pattern, "*") + + default: + panic("chi: unknown node type") + } + + xpattern = pattern[idx:] + if len(xpattern) == 0 { + return true + } + + return n.findPattern(xpattern) + } + return false +} + +func (n *node) routes() []Route { + rts := []Route{} + + n.walk(func(eps endpoints, subroutes Routes) bool { + if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { + return false + } + + // Group methodHandlers by unique patterns + pats := make(map[string]endpoints) + + for mt, h := range eps { + if h.pattern == "" { + continue + } + p, ok := pats[h.pattern] + if !ok { + p = endpoints{} + pats[h.pattern] = p + } + p[mt] = h + } + + for p, mh := range pats { + hs := make(map[string]http.Handler) + if mh[mALL] != nil && mh[mALL].handler != nil { + hs["*"] = mh[mALL].handler + } + + for mt, h := range mh { + if h.handler == nil { + continue + } + m := methodTypString(mt) + if m == "" { + continue + } + hs[m] = h.handler + } + + rt := Route{p, hs, subroutes} + rts = append(rts, rt) + } + + return false + }) + + return rts +} + +func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { + // Visit the leaf values if any + if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { + return true + } + + // Recurse on the children + for _, ns := range n.children { + for _, cn := range ns { + if cn.walk(fn) { + return true + } + } + } + return false +} + +// patNextSegment returns the next segment details from a pattern: +// node type, param key, regexp string, param tail byte, param starting index, param ending index +func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { + ps := strings.Index(pattern, "{") + ws := strings.Index(pattern, "*") + + if ps < 0 && ws < 0 { + return ntStatic, "", "", 0, 0, len(pattern) // we return the entire thing + } + + // Sanity check + if ps >= 0 && ws >= 0 && ws < ps { + panic("chi: wildcard '*' must be the last pattern in a route, otherwise use a '{param}'") + } + + var tail byte = '/' // Default endpoint tail to / byte + + if ps >= 0 { + // Param/Regexp pattern is next + nt := ntParam + + // Read to closing } taking into account opens and closes in curl count (cc) + cc := 0 + pe := ps + for i, c := range pattern[ps:] { + if c == '{' { + cc++ + } else if c == '}' { + cc-- + if cc == 0 { + pe = ps + i + break + } + } + } + if pe == ps { + panic("chi: route param closing delimiter '}' is missing") + } + + key := pattern[ps+1 : pe] + pe++ // set end to next position + + if pe < len(pattern) { + tail = pattern[pe] + } + + var rexpat string + if idx := strings.Index(key, ":"); idx >= 0 { + nt = ntRegexp + rexpat = key[idx+1:] + key = key[:idx] + } + + if len(rexpat) > 0 { + if rexpat[0] != '^' { + rexpat = "^" + rexpat + } + if rexpat[len(rexpat)-1] != '$' { + rexpat += "$" + } + } + + return nt, key, rexpat, tail, ps, pe + } + + // Wildcard pattern as finale + if ws < len(pattern)-1 { + panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead") + } + return ntCatchAll, "*", "", 0, ws, len(pattern) +} + +func patParamKeys(pattern string) []string { + pat := pattern + paramKeys := []string{} + for { + ptyp, paramKey, _, _, _, e := patNextSegment(pat) + if ptyp == ntStatic { + return paramKeys + } + for i := 0; i < len(paramKeys); i++ { + if paramKeys[i] == paramKey { + panic(fmt.Sprintf("chi: routing pattern '%s' contains duplicate param key, '%s'", pattern, paramKey)) + } + } + paramKeys = append(paramKeys, paramKey) + pat = pat[e:] + } +} + +// longestPrefix finds the length of the shared prefix +// of two strings +func longestPrefix(k1, k2 string) int { + max := len(k1) + if l := len(k2); l < max { + max = l + } + var i int + for i = 0; i < max; i++ { + if k1[i] != k2[i] { + break + } + } + return i +} + +func methodTypString(method methodTyp) string { + for s, t := range methodMap { + if method == t { + return s + } + } + return "" +} + +type nodes []*node + +// Sort the list of nodes by label +func (ns nodes) Sort() { sort.Sort(ns); ns.tailSort() } +func (ns nodes) Len() int { return len(ns) } +func (ns nodes) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } +func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } + +// tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. +// The list order determines the traversal order. +func (ns nodes) tailSort() { + for i := len(ns) - 1; i >= 0; i-- { + if ns[i].typ > ntStatic && ns[i].tail == '/' { + ns.Swap(i, len(ns)-1) + return + } + } +} + +func (ns nodes) findEdge(label byte) *node { + num := len(ns) + idx := 0 + i, j := 0, num-1 + for i <= j { + idx = i + (j-i)/2 + if label > ns[idx].label { + i = idx + 1 + } else if label < ns[idx].label { + j = idx - 1 + } else { + i = num // breaks cond + } + } + if ns[idx].label != label { + return nil + } + return ns[idx] +} + +// Route describes the details of a routing handler. +// Handlers map key is an HTTP method +type Route struct { + Pattern string + Handlers map[string]http.Handler + SubRoutes Routes +} + +// WalkFunc is the type of the function called for each method and route visited by Walk. +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// Walk walks any router tree that implements Routes interface. +func Walk(r Routes, walkFn WalkFunc) error { + return walk(r, walkFn, "") +} + +func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { + for _, route := range r.Routes() { + mws := make([]func(http.Handler) http.Handler, len(parentMw)) + copy(mws, parentMw) + mws = append(mws, r.Middlewares()...) + + if route.SubRoutes != nil { + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { + return err + } + continue + } + + for method, handler := range route.Handlers { + if method == "*" { + // Ignore a "catchAll" method, since we pass down all the specific methods for each route. + continue + } + + fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + + if chain, ok := handler.(*ChainHandler); ok { + if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { + return err + } + } else { + if err := walkFn(method, fullRoute, handler, mws...); err != nil { + return err + } + } + } + } + + return nil +} diff --git a/project/9-scraper/src/vendor/github.com/go-chi/cors/LICENSE b/project/9-scraper/src/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 0000000..aee6182 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/project/9-scraper/src/vendor/github.com/go-chi/cors/README.md b/project/9-scraper/src/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 0000000..b41686b --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/project/9-scraper/src/vendor/github.com/go-chi/cors/cors.go b/project/9-scraper/src/vendor/github.com/go-chi/cors/cors.go new file mode 100644 index 0000000..8df8163 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/cors/cors.go @@ -0,0 +1,400 @@ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. +package cors + +import ( + "log" + "net/http" + "os" + "strconv" + "strings" +) + +// Options is a configuration container to setup the CORS middleware. +type Options struct { + // AllowedOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // An origin may contain a wildcard (*) to replace 0 or more characters + // (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penalty. + // Only one wildcard can be used per origin. + // Default value is ["*"] + AllowedOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowedOrigins is ignored. + AllowOriginFunc func(r *http.Request, origin string) bool + + // AllowedMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (HEAD, GET and POST). + AllowedMethods []string + + // AllowedHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + // If the special "*" value is present in the list, all headers will be allowed. + // Default value is [] but "Origin" is always appended to the list. + AllowedHeaders []string + + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposedHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + + // OptionsPassthrough instructs preflight to let other potential next handlers to + // process the OPTIONS method. Turn this on if your application handles OPTIONS. + OptionsPassthrough bool + + // Debugging flag adds additional output to debug server side CORS issues + Debug bool +} + +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + +// Cors http handler +type Cors struct { + // Debug logger + Log Logger + + // Normalized list of plain allowed origins + allowedOrigins []string + + // List of allowed origins containing wildcards + allowedWOrigins []wildcard + + // Optional origin validator function + allowOriginFunc func(r *http.Request, origin string) bool + + // Normalized list of allowed headers + allowedHeaders []string + + // Normalized list of allowed methods + allowedMethods []string + + // Normalized list of exposed headers + exposedHeaders []string + maxAge int + + // Set to true when allowed origins contains a "*" + allowedOriginsAll bool + + // Set to true when allowed headers contains a "*" + allowedHeadersAll bool + + allowCredentials bool + optionPassthrough bool +} + +// New creates a new Cors handler with the provided options. +func New(options Options) *Cors { + c := &Cors{ + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, + } + if options.Debug && c.Log == nil { + c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) + } + + // Normalize options + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may error prone, we chose to ignore the spec here. + + // Allowed Origins + if len(options.AllowedOrigins) == 0 { + if options.AllowOriginFunc == nil { + // Default is all origins + c.allowedOriginsAll = true + } + } else { + c.allowedOrigins = []string{} + c.allowedWOrigins = []wildcard{} + for _, origin := range options.AllowedOrigins { + // Normalize + origin = strings.ToLower(origin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedOriginsAll = true + c.allowedOrigins = nil + c.allowedWOrigins = nil + break + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } + } + } + + // Allowed Headers + if len(options.AllowedHeaders) == 0 { + // Use sensible defaults + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} + } else { + // Origin is always appended as some browsers will always request for this header at preflight + c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) + for _, h := range options.AllowedHeaders { + if h == "*" { + c.allowedHeadersAll = true + c.allowedHeaders = nil + break + } + } + } + + // Allowed Methods + if len(options.AllowedMethods) == 0 { + // Default is spec's "simple" methods + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} + } else { + c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) + } + + return c +} + +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler +} + +// AllowAll create a new Cors handler with permissive configuration allowing all +// origins with all standard methods with any header and credentials. +func AllowAll() *Cors { + return New(Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, + AllowedHeaders: []string{"*"}, + AllowCredentials: false, + }) +} + +// Handler apply the CORS specification on the request, and add relevant CORS headers +// as necessary. +func (c *Cors) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + c.logf("Handler: Preflight request") + c.handlePreflight(w, r) + // Preflight requests are standalone and should stop the chain as some other + // middleware may not handle OPTIONS requests correctly. One typical example + // is authentication middleware ; OPTIONS requests won't carry authentication + // headers (see #1) + if c.optionPassthrough { + next.ServeHTTP(w, r) + } else { + w.WriteHeader(http.StatusOK) + } + } else { + c.logf("Handler: Actual request") + c.handleActualRequest(w, r) + next.ServeHTTP(w, r) + } + }) +} + +// handlePreflight handles pre-flight CORS requests +func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + if r.Method != http.MethodOptions { + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) + return + } + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + + if origin == "" { + c.logf("Preflight aborted: empty origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Preflight aborted: origin '%s' not allowed", origin) + return + } + + reqMethod := r.Header.Get("Access-Control-Request-Method") + if !c.isMethodAllowed(reqMethod) { + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) + return + } + reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) + if !c.areHeadersAllowed(reqHeaders) { + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + // Spec says: Since the list of methods can be unbounded, simply returning the method indicated + // by Access-Control-Request-Method (if supported) can be enough + headers.Set("Access-Control-Allow-Methods", strings.ToUpper(reqMethod)) + if len(reqHeaders) > 0 { + + // Spec says: Since the list of headers can be unbounded, simply returning supported headers + // from Access-Control-Request-Headers can be enough + headers.Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if c.maxAge > 0 { + headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) + } + c.logf("Preflight response headers: %v", headers) +} + +// handleActualRequest handles simple cross-origin requests, actual request or redirects +func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { + headers := w.Header() + origin := r.Header.Get("Origin") + + // Always set Vary, see https://github.com/rs/cors/issues/10 + headers.Add("Vary", "Origin") + if origin == "" { + c.logf("Actual request no headers added: missing origin") + return + } + if !c.isOriginAllowed(r, origin) { + c.logf("Actual request no headers added: origin '%s' not allowed", origin) + return + } + + // Note that spec does define a way to specifically disallow a simple method like GET or + // POST. Access-Control-Allow-Methods is only used for pre-flight requests and the + // spec doesn't instruct to check the allowed methods for simple cross-origin requests. + // We think it's a nice feature to be able to have control on those methods though. + if !c.isMethodAllowed(r.Method) { + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) + + return + } + if c.allowedOriginsAll { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Access-Control-Allow-Origin", origin) + } + if len(c.exposedHeaders) > 0 { + headers.Set("Access-Control-Expose-Headers", strings.Join(c.exposedHeaders, ", ")) + } + if c.allowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + c.logf("Actual response added headers: %v", headers) +} + +// convenience method. checks if a logger is set. +func (c *Cors) logf(format string, a ...interface{}) { + if c.Log != nil { + c.Log.Printf(format, a...) + } +} + +// isOriginAllowed checks if a given origin is allowed to perform cross-domain requests +// on the endpoint +func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { + if c.allowOriginFunc != nil { + return c.allowOriginFunc(r, origin) + } + if c.allowedOriginsAll { + return true + } + origin = strings.ToLower(origin) + for _, o := range c.allowedOrigins { + if o == origin { + return true + } + } + for _, w := range c.allowedWOrigins { + if w.match(origin) { + return true + } + } + return false +} + +// isMethodAllowed checks if a given method can be used as part of a cross-domain request +// on the endpoint +func (c *Cors) isMethodAllowed(method string) bool { + if len(c.allowedMethods) == 0 { + // If no method allowed, always return false, even for preflight request + return false + } + method = strings.ToUpper(method) + if method == http.MethodOptions { + // Always allow preflight requests + return true + } + for _, m := range c.allowedMethods { + if m == method { + return true + } + } + return false +} + +// areHeadersAllowed checks if a given list of headers are allowed to used within +// a cross-domain request. +func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { + if c.allowedHeadersAll || len(requestedHeaders) == 0 { + return true + } + for _, header := range requestedHeaders { + header = http.CanonicalHeaderKey(header) + found := false + for _, h := range c.allowedHeaders { + if h == header { + found = true + break + } + } + if !found { + return false + } + } + return true +} diff --git a/project/9-scraper/src/vendor/github.com/go-chi/cors/utils.go b/project/9-scraper/src/vendor/github.com/go-chi/cors/utils.go new file mode 100644 index 0000000..3fe5a5a --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/go-chi/cors/utils.go @@ -0,0 +1,70 @@ +package cors + +import "strings" + +const toLower = 'a' - 'A' + +type converter func(string) string + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix+w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} + +// convert converts a list of string using the passed converter function +func convert(s []string, c converter) []string { + out := []string{} + for _, i := range s { + out = append(out, c(i)) + } + return out +} + +// parseHeaderList tokenize + normalize a string containing a list of headers +func parseHeaderList(headerList string) []string { + l := len(headerList) + h := make([]byte, 0, l) + upper := true + // Estimate the number headers in order to allocate the right splice size + t := 0 + for i := 0; i < l; i++ { + if headerList[i] == ',' { + t++ + } + } + headers := make([]string, 0, t) + for i := 0; i < l; i++ { + b := headerList[i] + if b >= 'a' && b <= 'z' { + if upper { + h = append(h, b-toLower) + } else { + h = append(h, b) + } + } else if b >= 'A' && b <= 'Z' { + if !upper { + h = append(h, b+toLower) + } else { + h = append(h, b) + } + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { + h = append(h, b) + } + + if b == ' ' || b == ',' || i == l-1 { + if len(h) > 0 { + // Flush the found header + headers = append(headers, string(h)) + h = h[:0] + upper = true + } + } else { + upper = b == '-' + } + } + return headers +} diff --git a/project/9-scraper/src/vendor/github.com/joho/godotenv/.gitignore b/project/9-scraper/src/vendor/github.com/joho/godotenv/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/joho/godotenv/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/project/9-scraper/src/vendor/github.com/joho/godotenv/LICENCE b/project/9-scraper/src/vendor/github.com/joho/godotenv/LICENCE new file mode 100644 index 0000000..e7ddd51 --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/joho/godotenv/LICENCE @@ -0,0 +1,23 @@ +Copyright (c) 2013 John Barton + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/project/9-scraper/src/vendor/github.com/joho/godotenv/README.md b/project/9-scraper/src/vendor/github.com/joho/godotenv/README.md new file mode 100644 index 0000000..bfbe66a --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/joho/godotenv/README.md @@ -0,0 +1,202 @@ +# GoDotEnv ![CI](https://github.com/joho/godotenv/workflows/CI/badge.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/joho/godotenv)](https://goreportcard.com/report/github.com/joho/godotenv) + +A Go (golang) port of the Ruby [dotenv](https://github.com/bkeepers/dotenv) project (which loads env vars from a .env file). + +From the original Library: + +> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables. +> +> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped. + +It can be used as a library (for loading in env for your own daemons etc.) or as a bin command. + +There is test coverage and CI for both linuxish and Windows environments, but I make no guarantees about the bin version working on Windows. + +## Installation + +As a library + +```shell +go get github.com/joho/godotenv +``` + +or if you want to use it as a bin command + +go >= 1.17 +```shell +go install github.com/joho/godotenv/cmd/godotenv@latest +``` + +go < 1.17 +```shell +go get github.com/joho/godotenv/cmd/godotenv +``` + +## Usage + +Add your application configuration to your `.env` file in the root of your project: + +```shell +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE +``` + +Then in your Go app you can do something like + +```go +package main + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func main() { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + s3Bucket := os.Getenv("S3_BUCKET") + secretKey := os.Getenv("SECRET_KEY") + + // now do something with s3 or whatever +} +``` + +If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import + +```go +import _ "github.com/joho/godotenv/autoload" +``` + +While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit + +```go +godotenv.Load("somerandomfile") +godotenv.Load("filenumberone.env", "filenumbertwo.env") +``` + +If you want to be really fancy with your env file you can do comments and exports (below is a valid env file) + +```shell +# I am a comment and that is OK +SOME_VAR=someval +FOO=BAR # comments at line end are OK too +export BAR=BAZ +``` + +Or finally you can do YAML(ish) style + +```yaml +FOO: bar +BAR: baz +``` + +as a final aside, if you don't want godotenv munging your env you can just get a map back instead + +```go +var myEnv map[string]string +myEnv, err := godotenv.Read() + +s3Bucket := myEnv["S3_BUCKET"] +``` + +... or from an `io.Reader` instead of a local file + +```go +reader := getRemoteFile() +myEnv, err := godotenv.Parse(reader) +``` + +... or from a `string` if you so desire + +```go +content := getRemoteFileContent() +myEnv, err := godotenv.Unmarshal(content) +``` + +### Precedence & Conventions + +Existing envs take precedence of envs that are loaded later. + +The [convention](https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use) +for managing multiple environments (i.e. development, test, production) +is to create an env named `{YOURAPP}_ENV` and load envs in this order: + +```go +env := os.Getenv("FOO_ENV") +if "" == env { + env = "development" +} + +godotenv.Load(".env." + env + ".local") +if "test" != env { + godotenv.Load(".env.local") +} +godotenv.Load(".env." + env) +godotenv.Load() // The Original .env +``` + +If you need to, you can also use `godotenv.Overload()` to defy this convention +and overwrite existing envs instead of only supplanting them. Use with caution. + +### Command Mode + +Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH` + +``` +godotenv -f /some/path/to/.env some_command with some args +``` + +If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD` + +By default, it won't override existing environment variables; you can do that with the `-o` flag. + +### Writing Env Files + +Godotenv can also write a map representing the environment to a correctly-formatted and escaped file + +```go +env, err := godotenv.Unmarshal("KEY=value") +err := godotenv.Write(env, "./.env") +``` + +... or to a string + +```go +env, err := godotenv.Unmarshal("KEY=value") +content, err := godotenv.Marshal(env) +``` + +## Contributing + +Contributions are welcome, but with some caveats. + +This library has been declared feature complete (see [#182](https://github.com/joho/godotenv/issues/182) for background) and will not be accepting issues or pull requests adding new functionality or breaking the library API. + +Contributions would be gladly accepted that: + +* bring this library's parsing into closer compatibility with the mainline dotenv implementations, in particular [Ruby's dotenv](https://github.com/bkeepers/dotenv) and [Node.js' dotenv](https://github.com/motdotla/dotenv) +* keep the library up to date with the go ecosystem (ie CI bumps, documentation changes, changes in the core libraries) +* bug fixes for use cases that pertain to the library's purpose of easing development of codebases deployed into twelve factor environments + +*code changes without tests and references to peer dotenv implementations will not be accepted* + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + +## Releases + +Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`. + +Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1` + +## Who? + +The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library. diff --git a/project/9-scraper/src/vendor/github.com/joho/godotenv/godotenv.go b/project/9-scraper/src/vendor/github.com/joho/godotenv/godotenv.go new file mode 100644 index 0000000..61b0ebb --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/joho/godotenv/godotenv.go @@ -0,0 +1,228 @@ +// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv) +// +// Examples/readme can be found on the GitHub page at https://github.com/joho/godotenv +// +// The TL;DR is that you make a .env file that looks something like +// +// SOME_ENV_VAR=somevalue +// +// and then in your go code you can call +// +// godotenv.Load() +// +// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR") +package godotenv + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +const doubleQuoteSpecialChars = "\\\n\r\"!$`" + +// Parse reads an env file from io.Reader, returning a map of keys and values. +func Parse(r io.Reader) (map[string]string, error) { + var buf bytes.Buffer + _, err := io.Copy(&buf, r) + if err != nil { + return nil, err + } + + return UnmarshalBytes(buf.Bytes()) +} + +// Load will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Load without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Load("fileone", "filetwo") +// +// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults. +func Load(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, false) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Overload will read your env file(s) and load them into ENV for this process. +// +// Call this function as close as possible to the start of your program (ideally in main). +// +// If you call Overload without any args it will default to loading .env in the current path. +// +// You can otherwise tell it which files to load (there can be more than one) like: +// +// godotenv.Overload("fileone", "filetwo") +// +// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefully set all vars. +func Overload(filenames ...string) (err error) { + filenames = filenamesOrDefault(filenames) + + for _, filename := range filenames { + err = loadFile(filename, true) + if err != nil { + return // return early on a spazout + } + } + return +} + +// Read all env (with same file loading semantics as Load) but return values as +// a map rather than automatically writing values into env +func Read(filenames ...string) (envMap map[string]string, err error) { + filenames = filenamesOrDefault(filenames) + envMap = make(map[string]string) + + for _, filename := range filenames { + individualEnvMap, individualErr := readFile(filename) + + if individualErr != nil { + err = individualErr + return // return early on a spazout + } + + for key, value := range individualEnvMap { + envMap[key] = value + } + } + + return +} + +// Unmarshal reads an env file from a string, returning a map of keys and values. +func Unmarshal(str string) (envMap map[string]string, err error) { + return UnmarshalBytes([]byte(str)) +} + +// UnmarshalBytes parses env file from byte slice of chars, returning a map of keys and values. +func UnmarshalBytes(src []byte) (map[string]string, error) { + out := make(map[string]string) + err := parseBytes(src, out) + + return out, err +} + +// Exec loads env vars from the specified filenames (empty map falls back to default) +// then executes the cmd specified. +// +// Simply hooks up os.Stdin/err/out to the command and calls Run(). +// +// If you want more fine grained control over your command it's recommended +// that you use `Load()`, `Overload()` or `Read()` and the `os/exec` package yourself. +func Exec(filenames []string, cmd string, cmdArgs []string, overload bool) error { + op := Load + if overload { + op = Overload + } + if err := op(filenames...); err != nil { + return err + } + + command := exec.Command(cmd, cmdArgs...) + command.Stdin = os.Stdin + command.Stdout = os.Stdout + command.Stderr = os.Stderr + return command.Run() +} + +// Write serializes the given environment and writes it to a file. +func Write(envMap map[string]string, filename string) error { + content, err := Marshal(envMap) + if err != nil { + return err + } + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(content + "\n") + if err != nil { + return err + } + return file.Sync() +} + +// Marshal outputs the given environment as a dotenv-formatted environment file. +// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped. +func Marshal(envMap map[string]string) (string, error) { + lines := make([]string, 0, len(envMap)) + for k, v := range envMap { + if d, err := strconv.Atoi(v); err == nil { + lines = append(lines, fmt.Sprintf(`%s=%d`, k, d)) + } else { + lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v))) + } + } + sort.Strings(lines) + return strings.Join(lines, "\n"), nil +} + +func filenamesOrDefault(filenames []string) []string { + if len(filenames) == 0 { + return []string{".env"} + } + return filenames +} + +func loadFile(filename string, overload bool) error { + envMap, err := readFile(filename) + if err != nil { + return err + } + + currentEnv := map[string]bool{} + rawEnv := os.Environ() + for _, rawEnvLine := range rawEnv { + key := strings.Split(rawEnvLine, "=")[0] + currentEnv[key] = true + } + + for key, value := range envMap { + if !currentEnv[key] || overload { + _ = os.Setenv(key, value) + } + } + + return nil +} + +func readFile(filename string) (envMap map[string]string, err error) { + file, err := os.Open(filename) + if err != nil { + return + } + defer file.Close() + + return Parse(file) +} + +func doubleQuoteEscape(line string) string { + for _, c := range doubleQuoteSpecialChars { + toReplace := "\\" + string(c) + if c == '\n' { + toReplace = `\n` + } + if c == '\r' { + toReplace = `\r` + } + line = strings.Replace(line, string(c), toReplace, -1) + } + return line +} diff --git a/project/9-scraper/src/vendor/github.com/joho/godotenv/parser.go b/project/9-scraper/src/vendor/github.com/joho/godotenv/parser.go new file mode 100644 index 0000000..cc709af --- /dev/null +++ b/project/9-scraper/src/vendor/github.com/joho/godotenv/parser.go @@ -0,0 +1,271 @@ +package godotenv + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "unicode" +) + +const ( + charComment = '#' + prefixSingleQuote = '\'' + prefixDoubleQuote = '"' + + exportPrefix = "export" +) + +func parseBytes(src []byte, out map[string]string) error { + src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1) + cutset := src + for { + cutset = getStatementStart(cutset) + if cutset == nil { + // reached end of file + break + } + + key, left, err := locateKeyName(cutset) + if err != nil { + return err + } + + value, left, err := extractVarValue(left, out) + if err != nil { + return err + } + + out[key] = value + cutset = left + } + + return nil +} + +// getStatementPosition returns position of statement begin. +// +// It skips any comment line or non-whitespace character. +func getStatementStart(src []byte) []byte { + pos := indexOfNonSpaceChar(src) + if pos == -1 { + return nil + } + + src = src[pos:] + if src[0] != charComment { + return src + } + + // skip comment section + pos = bytes.IndexFunc(src, isCharFunc('\n')) + if pos == -1 { + return nil + } + + return getStatementStart(src[pos:]) +} + +// locateKeyName locates and parses key name and returns rest of slice +func locateKeyName(src []byte) (key string, cutset []byte, err error) { + // trim "export" and space at beginning + src = bytes.TrimLeftFunc(src, isSpace) + if bytes.HasPrefix(src, []byte(exportPrefix)) { + trimmed := bytes.TrimPrefix(src, []byte(exportPrefix)) + if bytes.IndexFunc(trimmed, isSpace) == 0 { + src = bytes.TrimLeftFunc(trimmed, isSpace) + } + } + + // locate key name end and validate it in single loop + offset := 0 +loop: + for i, char := range src { + rchar := rune(char) + if isSpace(rchar) { + continue + } + + switch char { + case '=', ':': + // library also supports yaml-style value declaration + key = string(src[0:i]) + offset = i + 1 + break loop + case '_': + default: + // variable name should match [A-Za-z0-9_.] + if unicode.IsLetter(rchar) || unicode.IsNumber(rchar) || rchar == '.' { + continue + } + + return "", nil, fmt.Errorf( + `unexpected character %q in variable name near %q`, + string(char), string(src)) + } + } + + if len(src) == 0 { + return "", nil, errors.New("zero length string") + } + + // trim whitespace + key = strings.TrimRightFunc(key, unicode.IsSpace) + cutset = bytes.TrimLeftFunc(src[offset:], isSpace) + return key, cutset, nil +} + +// extractVarValue extracts variable value and returns rest of slice +func extractVarValue(src []byte, vars map[string]string) (value string, rest []byte, err error) { + quote, hasPrefix := hasQuotePrefix(src) + if !hasPrefix { + // unquoted value - read until end of line + endOfLine := bytes.IndexFunc(src, isLineEnd) + + // Hit EOF without a trailing newline + if endOfLine == -1 { + endOfLine = len(src) + + if endOfLine == 0 { + return "", nil, nil + } + } + + // Convert line to rune away to do accurate countback of runes + line := []rune(string(src[0:endOfLine])) + + // Assume end of line is end of var + endOfVar := len(line) + if endOfVar == 0 { + return "", src[endOfLine:], nil + } + + // Work backwards to check if the line ends in whitespace then + // a comment (ie asdasd # some comment) + for i := endOfVar - 1; i >= 0; i-- { + if line[i] == charComment && i > 0 { + if isSpace(line[i-1]) { + endOfVar = i + break + } + } + } + + trimmed := strings.TrimFunc(string(line[0:endOfVar]), isSpace) + + return expandVariables(trimmed, vars), src[endOfLine:], nil + } + + // lookup quoted string terminator + for i := 1; i < len(src); i++ { + if char := src[i]; char != quote { + continue + } + + // skip escaped quote symbol (\" or \', depends on quote) + if prevChar := src[i-1]; prevChar == '\\' { + continue + } + + // trim quotes + trimFunc := isCharFunc(rune(quote)) + value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc)) + if quote == prefixDoubleQuote { + // unescape newlines for double quote (this is compat feature) + // and expand environment variables + value = expandVariables(expandEscapes(value), vars) + } + + return value, src[i+1:], nil + } + + // return formatted error if quoted string is not terminated + valEndIndex := bytes.IndexFunc(src, isCharFunc('\n')) + if valEndIndex == -1 { + valEndIndex = len(src) + } + + return "", nil, fmt.Errorf("unterminated quoted value %s", src[:valEndIndex]) +} + +func expandEscapes(str string) string { + out := escapeRegex.ReplaceAllStringFunc(str, func(match string) string { + c := strings.TrimPrefix(match, `\`) + switch c { + case "n": + return "\n" + case "r": + return "\r" + default: + return match + } + }) + return unescapeCharsRegex.ReplaceAllString(out, "$1") +} + +func indexOfNonSpaceChar(src []byte) int { + return bytes.IndexFunc(src, func(r rune) bool { + return !unicode.IsSpace(r) + }) +} + +// hasQuotePrefix reports whether charset starts with single or double quote and returns quote character +func hasQuotePrefix(src []byte) (prefix byte, isQuored bool) { + if len(src) == 0 { + return 0, false + } + + switch prefix := src[0]; prefix { + case prefixDoubleQuote, prefixSingleQuote: + return prefix, true + default: + return 0, false + } +} + +func isCharFunc(char rune) func(rune) bool { + return func(v rune) bool { + return v == char + } +} + +// isSpace reports whether the rune is a space character but not line break character +// +// this differs from unicode.IsSpace, which also applies line break as space +func isSpace(r rune) bool { + switch r { + case '\t', '\v', '\f', '\r', ' ', 0x85, 0xA0: + return true + } + return false +} + +func isLineEnd(r rune) bool { + if r == '\n' || r == '\r' { + return true + } + return false +} + +var ( + escapeRegex = regexp.MustCompile(`\\.`) + expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`) + unescapeCharsRegex = regexp.MustCompile(`\\([^$])`) +) + +func expandVariables(v string, m map[string]string) string { + return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string { + submatch := expandVarRegex.FindStringSubmatch(s) + + if submatch == nil { + return s + } + if submatch[1] == "\\" || submatch[2] == "(" { + return submatch[0][1:] + } else if submatch[4] != "" { + return m[submatch[4]] + } + return s + }) +} diff --git a/project/9-scraper/src/vendor/modules.txt b/project/9-scraper/src/vendor/modules.txt new file mode 100644 index 0000000..6ebd5e2 --- /dev/null +++ b/project/9-scraper/src/vendor/modules.txt @@ -0,0 +1,9 @@ +# github.com/go-chi/chi v1.5.4 +## explicit; go 1.16 +github.com/go-chi/chi +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors +# github.com/joho/godotenv v1.5.1 +## explicit; go 1.12 +github.com/joho/godotenv