Skip to content

Commit

Permalink
Add implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
sepetrov committed Oct 18, 2019
1 parent 89ad669 commit 5badddf
Show file tree
Hide file tree
Showing 5 changed files with 384 additions and 1 deletion.
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.PHONY: all build clean help version

.DEFAULT_GOAL: build

#
# Variables
#

ARCHITECTURES=386 amd64
BINARY=wikiipsum
PLATFORMS=darwin linux windows
VERSION = $(shell \
git -C . describe --tags 2> /dev/null || \
git -C . rev-parse --short HEAD 2> /dev/null || \
echo "unknown" \
)

#
# Targets
#

clean: ## Remove binary files.
rm -fv $(BINARY)*

## Compile binaries for all OS.
all:
$(foreach GOOS, $(PLATFORMS), $(foreach GOARCH, $(ARCHITECTURES), $(shell export GOOS=$(GOOS); export GOARCH=$(GOARCH); go build -o $(BINARY)-$(VERSION)-$(GOOS)-$(GOARCH) -ldflags="-X 'main.Version=$(VERSION)'")))

build: ## Compile a binary.
go build -o $(BINARY) -ldflags="-X 'main.Version=$(VERSION)'"

help: ## Show help.
@echo
@echo ' Usage:'
@echo ' make <target>'
@echo
@echo ' Targets:'
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//'
@echo

version: ## Print version.
@echo $(VERSION)
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# wikiipsum
Like Lorem Ipsum but using Wikipedia

[Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum) generator using content from [Wikipedia](https://wikipedia.org).

---

A small program written in Go to generate random text in specified language using the [Wikimedia REST API](https://www.mediawiki.org/wiki/REST_API).

## Compilation

You need [Go](https://golang.org). Download and install it if you don't have it yet.

Clone the repository.
```bash
git clone git@github.com:sepetrov/wikiipsum.git .
```

Navigate to the directory with the source code.
```bash
cd wikiipsum
```

Compile a binary.
```bash
go build -o wikiipsum .
```

## Download

To download a precompiled binary visit the [releases page](https://github.com/sepetrov/wikiipsum/releases).

## Usage

```bash
Lorem Ipsum generates text using content from Wikipedia and prints it to the standard output.
For more information visit https://github.com/sepetrov/wikiipsum.

Example:

./wikiipsum -user-agent="admin@example.com" -lang="en" -length="500"

Usage of ./wikiipsum:
-lang string
Language code, e.g. 'en'
-length string
Length of generated text, e.g. '500' for 500 bytes, '100 bytes', '100 Kb', '1.5 MB' etc.
-rate float
Request rate limit in req/s
-user-agent string
User agent header for API calls to Wikipedia. It should provide information how to contact you, e.g. admin@example.com
-verbose
Verbose
```

## License

See [LICENSE](LICENSE).
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/sepetrov/wikiipsum

go 1.13

require (
github.com/cenkalti/backoff v2.2.1+incompatible
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 h1:xQwXv67TxFo9nC1GJFyab5eq/5B590r6RlnL/G8Sz7w=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
274 changes: 274 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"math"
"mime"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/cenkalti/backoff"
"golang.org/x/time/rate"
)

var usage = `Lorem Ipsum generates text using content from Wikipedia and prints it to the standard output.
For more information visit https://github.com/sepetrov/wikiipsum.
Example:
%s -user-agent="admin@example.com" -lang="en" -length="500"
Usage of %s:
`

// Version is the program version.
// See https://github.com/golang/go/wiki/GcToolchainTricks#including-build-information-in-the-executable.
var Version string

// See https://en.wikipedia.org/api/rest_v1/.
func main() {
var (
userAgent string
lang string
lengthStr string
rateLimit float64
verbose bool
version bool
)

flag.StringVar(&userAgent, "user-agent", "", "User agent header for API calls to Wikipedia. It should provide information how to contact you, e.g. admin@example.com")
flag.StringVar(&lang, "lang", "", "Language code, e.g. 'en'")
flag.StringVar(&lengthStr, "length", "", "Length of generated text, e.g. '500' for 500 bytes, '100 bytes', '100 Kb', '1.5 MB' etc.")
flag.Float64Var(&rateLimit, "rate", 0, "Request rate limit in req/s")
flag.BoolVar(&verbose, "verbose", false, "Verbose")
flag.BoolVar(&version, "version", false, "Print version")

flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0], os.Args[0])
flag.PrintDefaults()
}

flag.Parse()

if version {
fmt.Fprintln(os.Stdout, Version)
os.Exit(0)
}

if userAgent == "" {
fmt.Println("'-user-agent' is required")
os.Exit(1)
}
if lang == "" {
fmt.Println("'-lang' is required")
os.Exit(1)
}
length, err := str2bytes(lengthStr)
if err != nil {
fmt.Println("'-length' is invalid: " + err.Error())
os.Exit(1)
}
if rateLimit <= 0 || rateLimit > maxRateLimit {
rateLimit = maxRateLimit
}

if verbose {
fmt.Fprintf(os.Stderr, "Rate limit: %f\n", rateLimit)
}

wiki := wikiClient{
url: fmt.Sprintf(randomSummaryURL, lang),
userAgent: userAgent,
client: http.Client{Timeout: 5 * time.Second},
}

ctx, cancel := context.WithCancel(context.Background())
txtch := make(chan []byte) // Channel to send random text.
errch := make(chan error) // Channel to send errors.

go func() {
sleepch := make(chan time.Duration) // Channel to pause creating go routines.
lim := rate.NewLimiter(rate.Limit(rateLimit), 1)
for {

// Pause when we need to back off due to errors.
select {
case d := <-sleepch:
time.Sleep(d)
default:
}

// Wait for the next available event so we don't exceed the rate limit.
if err := lim.Wait(ctx); err != nil {
errch <- err
return
}

go func() {
op := func() error {
if verbose {
fmt.Fprint(os.Stderr, ".")
}
b, err := wiki.RandomSummary(ctx)
if errors.Is(err, errTooManyRequests) {
errch <- err
return err // Back off when we have 429 Too Many Requests response.
}
if err != nil {
errch <- err
return nil
}
txtch <- b
return nil
}
notify := func(_ error, next time.Duration) {
sleepch <- next
}

if err := backoff.RetryNotify(op, backoff.NewExponentialBackOff(), notify); err != nil {
errch <- err
}
}()
}
}()

sigch := make(chan os.Signal) // Channel to send OS signal to terminate this program.
l := 0
for {
select {
case txt := <-txtch:
n, _ := fmt.Fprintln(os.Stdout, string(txt))
if length > 0 {
l += n
if l >= length {
goto End
}
}
case err := <-errch:
var ignore bool
var timeoutErr interface {
Timeout() bool
}
if errors.Is(err, errTooManyRequests) ||
errors.Is(err, context.Canceled) ||
errors.Is(err, context.DeadlineExceeded) ||
errors.As(err, &timeoutErr) && timeoutErr.Timeout() {
ignore = true
}
if verbose || !ignore {
fmt.Fprintln(os.Stderr, err)
}
case sig := <-sigch:
if sig == os.Interrupt || sig == os.Kill {
goto End
}
}
}

End:
cancel()
}

// byteMap contains a map of regular expressions for conversion of strings to bytes.
var byteMap = map[*regexp.Regexp]float64{
regexp.MustCompile(`^(\d+)$`): 1,
regexp.MustCompile(`^(\d+)\s?(byte)$`): 1,
regexp.MustCompile(`^(\d+)\s?(byte)$`): 1,
regexp.MustCompile(`^(\d+)\s?(bytes)$`): 1,
regexp.MustCompile(`^(\d+(\.\d+)?)\s?(Kb)$`): 1024,
regexp.MustCompile(`^(\d+(\.\d+)?)\s?(MB)$`): 1024 * 1024,
}

// str2bytes parses s and tries to returns the corresponding size in bytes.
func str2bytes(s string) (int, error) {
for r, f := range byteMap {
match := r.FindStringSubmatch(s)
if len(match) == 0 {
continue
}
val, err := strconv.ParseFloat(match[1], 10)
if err != nil {
return 0, err
}
return int(math.Round(val * f)), nil
}

return 0, errors.New("cannot parse string")
}

// randomSummaryURL is the fmt.Sprintf pattern of the Wikipedia API URL
// for random page summary. Use two character language code as an argument.
//
// fmt.Sprintf(randomSummaryURL, "en")
const randomSummaryURL = "https://%s.wikipedia.org/api/rest_v1/page/random/summary"

// maxRateLimit is the maximum rate limit for API calls to Wikipedia.
// See https://en.wikipedia.org/api/rest_v1/.
const maxRateLimit float64 = 200

type wikiClient struct {
url string
userAgent string
client http.Client
}

var errTooManyRequests = errors.New("too many requests")

// RandomText returns text from a random Wikipedia page.
func (w *wikiClient) RandomSummary(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, w.url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept", "application/problem+json")
req.Header.Add("User-Agent", w.userAgent)

resp, err := w.client.Do(req)

if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
var timeoutErr interface {
Timeout() bool
}
if errors.As(err, &timeoutErr) && timeoutErr.Timeout() {
return nil, err
}
if err != nil {
return nil, err
}

if resp.StatusCode == http.StatusTooManyRequests {
return nil, errTooManyRequests
}

fail := func(format string, a ...interface{}) ([]byte, error) {
a = append(a, req, resp)
return nil, fmt.Errorf(format+"\n\nrequest:\n%v\n\nresponse:\n%v\n", a...)
}

if resp.StatusCode != http.StatusOK {
return fail("response status %s", resp.Status)
}

if ctype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil {
return nil, err
} else if ctype != "application/json" {
return fail("response content type %q", ctype)
}

var body struct{ Extract string `json:"extract"` }
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return fail("%w", err)
}

return []byte(strings.TrimSpace(body.Extract)), nil
}

0 comments on commit 5badddf

Please sign in to comment.