<commit_before>package main

import (
	"errors"
	"fmt"
	"mime"
	"strings"
	"sync"

	"github.com/xeipuuv/gojsonschema"
)

// ResponseExpectation is an interface to any validation
// that needs to be performed on response.
type ResponseExpectation interface {
	check(resp *Response) error
}

// StatusCodeExpectation validates response HTTP code.
type StatusCodeExpectation struct {
	statusCode int
}

func (e StatusCodeExpectation) check(resp *Response) error {
	if resp.http.StatusCode != e.statusCode {
		msg := fmt.Sprintf("Unexpected Status Code. Expected: %d, Actual: %d\n", e.statusCode, resp.http.StatusCode)
		return errors.New(msg)
	}
	return nil
}

// BodySchemaExpectation validates response body against schema.
// Content-Type header is used to identify either json schema or xsd is applied.
type BodySchemaExpectation struct {
	schemaURI string
}

func (e BodySchemaExpectation) check(resp *Response) error {
	contentType, _, _ := mime.ParseMediaType(resp.http.Header.Get("content-type"))

	if contentType == "application/json" {
		return e.checkJSON(resp)
	}

	return fmt.Errorf("Unsupported content type: %s", contentType)
}

var jsonSchemaCache = map[string]interface{}{}

func (e BodySchemaExpectation) checkJSON(resp *Response) error {
	// concurrent access to jsonSchemaCache (see 'w' CLI option)
	// TODO replace with concurrent map when go 1.9 released
	var mu sync.Mutex
	mu.Lock()
	if jsonSchemaCache[e.schemaURI] == nil {
		debug.Printf("Loading schema %s", e.schemaURI)

		schemaLoader := gojsonschema.NewReferenceLoader(e.schemaURI)
		schema, err := schemaLoader.LoadJSON()

		if err != nil {
			return fmt.Errorf("failed to load schema: %s", err)
		}

		jsonSchemaCache[e.schemaURI] = schema
	}
	mu.Unlock()

	schemaLoader := gojsonschema.NewGoLoader(jsonSchemaCache[e.schemaURI])
	documentLoader := gojsonschema.NewStringLoader(string(resp.body))

	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
	if err != nil {
		return fmt.Errorf("failed to load schema file: %s", err)
	}

	if !result.Valid() {
		msg := "Unexpected Body Schema:\n"
		for _, desc := range result.Errors() {
			msg = fmt.Sprintf(msg+"%s\n", desc)<commit_msg>fix concurrent map access issue (closing #50)<commit_after>package main

import (
	"errors"
	"fmt"
	"mime"
	"strings"
	"sync"

	"github.com/xeipuuv/gojsonschema"
)

// ResponseExpectation is an interface to any validation
// that needs to be performed on response.
type ResponseExpectation interface {
	check(resp *Response) error
}

// StatusCodeExpectation validates response HTTP code.
type StatusCodeExpectation struct {
	statusCode int
}

func (e StatusCodeExpectation) check(resp *Response) error {
	if resp.http.StatusCode != e.statusCode {
		msg := fmt.Sprintf("Unexpected Status Code. Expected: %d, Actual: %d\n", e.statusCode, resp.http.StatusCode)
		return errors.New(msg)
	}
	return nil
}

// BodySchemaExpectation validates response body against schema.
// Content-Type header is used to identify either json schema or xsd is applied.
type BodySchemaExpectation struct {
	schemaURI string
}

func (e BodySchemaExpectation) check(resp *Response) error {
	contentType, _, _ := mime.ParseMediaType(resp.http.Header.Get("content-type"))

	if contentType == "application/json" {
		return e.checkJSON(resp)
	}

	return fmt.Errorf("Unsupported content type: %s", contentType)
}

var jsonSchemaCache = map[string]interface{}{}

func (e BodySchemaExpectation) checkJSON(resp *Response) error {
	// concurrent access to jsonSchemaCache (see 'w' CLI option)
	// TODO replace with concurrent map when go 1.9 released
	var safeSchema interface{}

	var mu sync.Mutex
	mu.Lock()
	if jsonSchemaCache[e.schemaURI] == nil {
		debug.Printf("Loading schema %s", e.schemaURI)

		schemaLoader := gojsonschema.NewReferenceLoader(e.schemaURI)
		schema, err := schemaLoader.LoadJSON()

		if err != nil {
			return fmt.Errorf("failed to load schema: %s", err)
		}

		jsonSchemaCache[e.schemaURI] = schema
	}
	safeSchema = jsonSchemaCache[e.schemaURI]
	mu.Unlock()

	schemaLoader := gojsonschema.NewGoLoader(safeSchema)
	documentLoader := gojsonschema.NewStringLoader(string(resp.body))

	result, err := gojsonschema.Validate(schemaLoader, documentLoader)
	if err != nil {
		return fmt.Errorf("failed to load schema file: %s", err)
	}

	if !result.Valid() {
		msg := "Unexpected Body Schema:\n"
		for _, desc := range result.Errors() {
			msg = fmt.Sprintf(msg+"%s\n", desc)
{'fraction_non_alphanumeric': 0.11637347767253045, 'fraction_numerical': 0.0013531799729364006, 'mean_word_length': 4.685897435897436, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5947105', 'n_tokens_mistral': 1445, 'n_tokens_neox': 1400, 'n_words': 394}