GithubHelp home page GithubHelp logo

Comments (10)

jonathanyuechun avatar jonathanyuechun commented on May 11, 2024 2

Wow this is really nice :)

i had to change the code a bit to avoid nil pointer panic

func opsPath(in map[string]*huma.PathItem) (r []*huma.Operation) {
	r = make([]*huma.Operation, 0)
	for _, v := range in {
		if v.Get != nil {
			r = append(r, v.Get)
		}
		if v.Post != nil {
			r = append(r, v.Post)
		}
		if v.Put != nil {
			r = append(r, v.Put)
		}
		if v.Patch != nil {
			r = append(r, v.Patch)
		}
		if v.Delete != nil {
			r = append(r, v.Delete)
		}
	}
	return
}

// replaceDocs crawls the component schemas and replaces relative links in the
// description with the contents of the file from the embedded FS.
func replaceDocs(doc *huma.OpenAPI) {
	for _, op := range opsPath(doc.Paths) {
		for _, param := range op.Parameters {
			checkSchema(param.Schema)
		}
	}
	for _, schema := range doc.Components.Schemas.Map() {
		checkSchema(schema)
	}
}

from huma.

danielgtaylor avatar danielgtaylor commented on May 11, 2024

@jonathanyuechun unfortunately the Go compiler strips comments, so I can't access these at runtime. This could be accomplished by analyzing the Go source code and generating additional code, but that introduces extra steps. You could go in after the fact and update the generated huma.Schema object in OpenAPI.Components.Schemas with extra docs, but that's not ideal either.

I'm open to ideas on this one and will think about ways this could be done.

FYI it is possible to use files/variables for doc/description of the OpenAPI itself and operations, just not struct fields.

from huma.

jonathanyuechun avatar jonathanyuechun commented on May 11, 2024

I see.

How about for a starter, providing an optional interface to implement/or a function that can enrich the schema by taking as input a ref to the struct we want to enrich , a huma schema
and the generated openapi obj that only overrides json schema fields provided by the end user, like a merge from user input on top of the default schema generator?

Another 3 banger features would also be the ability to

  1. add arbitrary schemas (dynamic) at runtime
  2. that can be referenced in description/doc section.
  3. Validate schema outside of http request in two ways
  • a ref t a struct and the json to validate
  • a json schema as string/byte and the json datq to validate

from huma.

danielgtaylor avatar danielgtaylor commented on May 11, 2024

I thought about this some more and I think what you want to do is actually possible today in Huma with a couple small utility functions since you have full access to the generated OpenAPI before service startup. Here's a quick example:

  1. Create some markdown files in a docs directory, e.g. docs/message.md.
  2. Use Go's built-in embed package and //go:embed docs/* to bundle docs into the executable (this makes deployment easier and would be optional).
  3. Use a sentinel starting value e.g. @ in the docs field tag to denote you want to load from a file, like doc:"@docs/message.md"
  4. Create a recursive utility to crawl the params and schemas, replacing @file... in schema descriptions with the contents of the file. Call this from your main func after registering all the operations.

This modified hello world example works for me if I create a docs directory and put some markdown files in it:

package main

import (
	"context"
	"embed"
	"fmt"
	"net/http"
	"strings"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
	"github.com/go-chi/chi/v5"
)

//go:embed docs/*
var docs embed.FS

// Options for the CLI.
type Options struct {
	Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
	Name string `path:"name" maxLength:"30" example:"world" doc:"@docs/name.md"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
	Body struct {
		Message string `json:"message" example:"Hello, world!" doc:"@docs/message.md"`
	}
}

func checkSchema(s *huma.Schema) {
	if strings.HasPrefix(s.Description, "@") {
		b, err := docs.ReadFile(s.Description[1:])
		if err != nil {
			panic(err)
		}
		s.Description = string(b)
	}

	// If it's an array, check the array entry type.
	if s.Items != nil {
		checkSchema(s.Items)
	}

	// If it's an object, check all the property types.
	for _, propSchema := range s.Properties {
		checkSchema(propSchema)
	}
}

// replaceDocs crawls the component schemas and replaces relative links in the
// description with the contents of the file from the embedded FS.
func replaceDocs(doc *huma.OpenAPI) {
	for _, path := range doc.Paths {
		for _, op := range []*huma.Operation{path.Get, path.Post, path.Put, path.Patch, path.Delete} {
			if op != nil {
				for _, param := range op.Parameters {
					checkSchema(param.Schema)
				}
			}
		}
	}
	for _, schema := range doc.Components.Schemas.Map() {
		checkSchema(schema)
	}
}

func main() {
	// Create a CLI app which takes a port option.
	cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) {
		// Create a new router & API
		router := chi.NewMux()
		api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

		// Register GET /greeting/{name}
		huma.Register(api, huma.Operation{
			OperationID: "get-greeting",
			Summary:     "Get a greeting",
			Method:      http.MethodGet,
			Path:        "/greeting/{name}",
		}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
			resp := &GreetingOutput{}
			resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
			return resp, nil
		})

		replaceDocs(api.OpenAPI())

		// Tell the CLI how to start your router.
		hooks.OnStart(func() {
			http.ListenAndServe(fmt.Sprintf(":%d", options.Port), router)
		})
	})

	// Run the CLI. When passed no commands, it starts the server.
	cli.Run()
}

As for your other feature ideas, you can already add arbitrary schemas for types to the registry. For example:

api.OpenAPI().Components.Schemas.Schema(reflect.TypeOf(MyCustomType{}), true, "FallbackNameHint")

You can also load schema objects like this (or manually instantiate *huma.Schema instances):

var s *huma.Schema
json.Unmarshal([]byte(`{"type": "string"}`), &s)

Then validate them outside of the normal request flow:

pb := huma.NewPathBuffer([]byte{}, 0)
res := &huma.ValidateResult{}

data := 1234 // this is the wrong type and will fail!
huma.Validate(api.OpenAPI().Components.Schemas, s, pb, huma.ModeReadFromServer, data, res)

for _, err := range res.Errors {
	fmt.Println(err.Error())
}
if len(res.Errors) > 0 {
	panic("validation failed")
}

Some of this is wrapped up in a small utility at https://huma.rocks/features/model-validation/.

from huma.

jonathanyuechun avatar jonathanyuechun commented on May 11, 2024

another small question,
is the json schema dynamic ref supported ?

from huma.

danielgtaylor avatar danielgtaylor commented on May 11, 2024

@jonathanyuechun if you're talking about $dynamicRef / $dynamicAnchor like in https://json-schema.org/blog/posts/dynamicref-and-generics then no, that's not something that is currently supported directly. You can use the schema extensions if you wanted to:

s := huma.Schema{
  Extensions: map[string]any{
    "$dynamicRef": "#T",
  },
}

The extensions are serialized in-line with the other props, so you can use this to set anything you want on the schema. The above schema s would serialize to YAML like:

$dynamicRef: "#T"

I'll be honest here and say I'm not super familiar with these advanced features, I haven't had a need for them myself, and I'm not sure how they would apply to schema generation from Go structs.

from huma.

jonathanyuechun avatar jonathanyuechun commented on May 11, 2024

No problem !
I dont have a use case right now that would need such a feature.

Last but not least.
I have a requirement:

I have a struct request that has a field whose type is a list of json objects.
Each object has a known set of common fields mapped against a schema. Their schema is mapped against a kind field of type string. Again, there are a known set if kind value and a set of unknown one (provided externally by end user)

My endgoal is to make all those schema

  1. Appear in openapi.json at runtime
  2. Huma validation works based on the enriched openapi

Do you know the best way to achieve this goal ?

from huma.

danielgtaylor avatar danielgtaylor commented on May 11, 2024

@jonathanyuechun sorry I'm not sure I understand the example. It sounds like maybe there is a discriminator field and you want one-of the schemas to match based on that field? Some examples might help... for example what would your schema look like and do you have some example inputs?

from huma.

jonathanyuechun avatar jonathanyuechun commented on May 11, 2024

yes of course

let's say i have this yaml (json)

things:
- kind: table
  legs: 4
  surface: 2M
- kind: house
  surface: 100
  units: M

So in this example, i have a list of objects where each kind may have a fixed known of fields.
For kind table, valid fields are legs of type integer and surface of type string.
For kind house, it shares a common property named surface but of different type (string != integer) with the kind table.
House also has a property units not present in table

These schemas cannot be defined as structs as they are not known in advance.
They are user provided inputs.

My end goal is to use them

  1. as validation method; i.e. Validate(jsonschemastring, userjsondatainputstring) [ schema_of_table, data: { "kind": "table", ... } ]
  2. add those json schema string to the already generated OpenAPI struct

from huma.

danielgtaylor avatar danielgtaylor commented on May 11, 2024

@jonathanyuechun I wanted to follow up on your latest question. I think that use case is quite complex. You may be able to use the pieces available in Huma to accomplish your desired goal, for example by using manually-created schemas and the validation utility functions available. However, this is more advanced than the out-of-the-box Huma supports, so there isn't a quick/easy function you can call.

My suggestion would be to load the schemas from strings (e.g. json unmarshal into *huma.Schema), make sure to call schema.PrecomputeMessages() and then you can use the huma.Validate(...) function with user-provided inputs you want to validate. In terms of adding to the OpenAPI, you can do that at any point via custom schemas during operation registration (huma.Register(...)) or after registration but before starting the server using the API config config.OpenAPI.Components.Schemas....

Good luck and let me know how else I can help. I'm going to close this issue but feel free to open new ones if things come up!

from huma.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.