acceptkit

Dependency-free HTTP representation negotiation toolkit for MoonBit.

http
accept
content-negotiation
headers
wasm
moon add XiaoMao-MYY/acceptkit@0.1.0
Download zip
Version
0.1.0
License
MIT
Last updated
8 days ago
Downloads
2
README

#acceptkit

acceptkit 是一个 MoonBit 原生、无外部依赖的 HTTP 表示协商工具库,用于解析 AcceptAccept-LanguageAccept-EncodingAccept-CharsetVaryCache-Control 和 ETag 条件请求相关头,并为服务端候选响应选择最合适的表示。

它解决的问题是:MoonBit Web 服务、Wasm HTTP 组件、接口 Mock、边缘函数和测试工具经常需要处理内容协商,但直接手写字符串判断容易漏掉 q 权重、通配符、显式拒绝、参数匹配、稳定排序、响应头生成和缓存相关边界。acceptkit 提供结构化解析结果、确定性的协商算法、候选矩阵和可读解释文本,便于在库、工具和示例工程中复用。

#适用场景

  • HTTP 服务端根据 Accept 选择 application/jsontext/htmlapplication/problem+json 等响应格式;
  • API 工具或 Mock 服务根据 Accept-Language 选择本地化语言;
  • 服务端根据 Accept-Encoding 选择 brgzipidentity
  • 服务端根据 Accept-Charset 选择 utf-8iso-8859-1 等字符集;
  • Web 框架中间件生成 Content-TypeContent-LanguageContent-EncodingVary
  • 测试工具记录候选矩阵,解释“为什么选择该响应格式”;
  • 缓存层检查 Cache-ControlVary 和 ETag 条件请求行为。

#安装

moon add XiaoMao-MYY/acceptkit

Mooncakes 包名:

XiaoMao-MYY/acceptkit

#最小使用示例

在使用方 moon.pkg 中导入:

import {
"XiaoMao-MYY/acceptkit" @acceptkit,
}

MoonBit 代码:

fn choose() -> String {
let result = @acceptkit.negotiate_response(
@acceptkit.RequestPreferences(
accept="application/json, text/html;q=0.6",
accept_language="zh-CN, en;q=0.8",
accept_encoding="br, gzip;q=0.7",
accept_charset="utf-8",
),
["text/html", "application/json"],
["en-US", "zh-CN"],
["gzip", "br"],
["utf-8"],
)
match result {
Ok(choice) => @acceptkit.response_header_block(choice.summary)
Err(_) => "invalid negotiation header"
}
}

#本地运行

moon check moon build moon test moon run cmd/main

示例入口会输出媒体类型协商、语言协商、组合响应协商和响应头结果。

#核心 API

  • parse_accept(header):解析 Accept 头,返回 MediaRange 列表;
  • parse_content_type(value):解析服务端候选媒体类型,返回 MediaOffer
  • negotiate_media(header, offers):在候选媒体类型中选择最佳结果;
  • parse_accept_language(header)negotiate_language(header, offers):解析语言偏好并选择语言;
  • parse_accept_encoding(header)negotiate_encoding(header, offers):解析压缩编码偏好并选择编码;
  • parse_accept_charset(header)negotiate_charset(header, offers):解析字符集偏好并选择字符集;
  • ServerVariantnegotiate_variant(preferences, variants):对媒体、语言、编码、字符集和服务端权重做组合协商;
  • build_response_variants(...)negotiate_response(...):从常见服务端候选数组生成变体并返回响应摘要;
  • response_headers(summary)response_header_block(summary):生成响应头;
  • parse_vary(value)merge_vary(left, right):解析、规范化和合并 Vary
  • parse_cache_policy(header)format_cache_summary(summary):提取缓存策略摘要;
  • parse_etag(value)evaluate_if_none_match(method, header, current)evaluate_if_match(header, current):处理 ETag 条件请求;
  • audit_preferences(preferences)audit_cache_control_header(header):输出可检查的风险提示;
  • negotiation_matrix_report(preferences, variants):生成候选矩阵报告。

#支持范围

  • Accept 中的 type/subtypetype/**/*
  • 媒体类型参数匹配,例如 text/html;level=1
  • q 权重,使用 0 到 1000 的整数表示,支持三位小数;
  • q=0 显式拒绝,且拒绝会优先于通配符回退;
  • 引号包裹的参数值,分割时会避开引号内的逗号和分号;
  • Accept-Language 基础语言范围、通配符和前缀匹配;
  • Accept-Encoding 的通配符、identity 默认可接受语义和显式拒绝;
  • Accept-Charset 常见别名规范化,例如 utf8latin1us_ascii
  • 组合变体协商、响应摘要、响应头生成和 406 摘要;
  • Vary 规范化、合并和缓存键风险审计;
  • Cache-Control 常见缓存指令摘要;
  • 强/弱 ETag 解析和 If-None-MatchIf-Match 判断;
  • 稳定排序:权重、具体度、参数数量、头部顺序、服务端权重、候选顺序。

#边界

  • 不解析完整 HTTP 报文;
  • 不做网络 I/O、文件 I/O 或框架绑定;
  • 不查询媒体类型注册表;
  • 不实现完整自然语言回退策略;
  • 不包含压缩、字符集转码或缓存存储实现。

#测试与验收命令

moon check moon build moon test moon run cmd/main moon publish --dry-run

#开源许可证和第三方说明

本项目采用 MIT License。核心实现为原创 MoonBit 代码,不移植第三方源码,不包含第三方素材。实现语义参考 HTTP 内容协商常见规则和 RFC 系列标准描述。

#
CacheDirective

pub(all) struct CacheDirective {
name : String
value : String?
order : Int
raw : String
} derive(Eq,
Debug
)

#
CachePolicySummary

pub(all) struct CachePolicySummary {
no_store : Bool
no_cache : Bool
is_public : Bool
is_private : Bool
must_revalidate : Bool
immutable : Bool
max_age : Int?
s_maxage : Int?
stale_while_revalidate : Int?
stale_if_error : Int?
unknown : Array[CacheDirective]
} derive(Eq,
Debug
)

#
CharsetCandidate

pub(all) struct CharsetCandidate {
offer : CharsetOffer
matched_range : CharsetRange
quality : Int
specificity : Int
reason : String
} derive(Eq,
Debug
)

#
CharsetDecision

pub(all) struct CharsetDecision {
selected : String?
quality : Int
candidate : CharsetCandidate?
matches : Array[CharsetCandidate]
explanation : String
} derive(Eq,
Debug
)

#
CharsetOffer

pub(all) struct CharsetOffer {
charset : String
canonical : String
index : Int
} derive(Eq,
Debug
)

#
CharsetRange

pub(all) struct CharsetRange {
charset : String
q : Int
order : Int
raw : String
} derive(Eq,
Debug
)

#
ConditionalDecision

pub(all) struct ConditionalDecision {
matched : Bool
status : Int
reason : String
} derive(Eq,
Debug
)

#
EncodingCandidate

pub(all) struct EncodingCandidate {
offer : EncodingOffer
matched_range : EncodingRange
quality : Int
specificity : Int
reason : String
} derive(Eq,
Debug
)

#
EncodingDecision

pub(all) struct EncodingDecision {
selected : String?
quality : Int
candidate : EncodingCandidate?
matches : Array[EncodingCandidate]
explanation : String
} derive(Eq,
Debug
)

#
EncodingOffer

pub(all) struct EncodingOffer {
coding : String
index : Int
} derive(Eq,
Debug
)

#
EncodingRange

pub(all) struct EncodingRange {
coding : String
q : Int
order : Int
raw : String
} derive(Eq,
Debug
)

#
EntityTag

pub(all) struct EntityTag {
weak : Bool
tag : String
raw : String
} derive(Eq,
Debug
)

#
HeaderAudit

pub(all) struct HeaderAudit {
ok : Bool
warnings : Array[String]
errors : Array[String]
} derive(Eq,
Debug
)

#
LanguageCandidate

pub(all) struct LanguageCandidate {
offer : LanguageOffer
matched_range : LanguageRange
quality : Int
specificity : Int
reason : String
} derive(Eq,
Debug
)

#
LanguageDecision

pub(all) struct LanguageDecision {
selected : String?
quality : Int
candidate : LanguageCandidate?
matches : Array[LanguageCandidate]
explanation : String
} derive(Eq,
Debug
)

#
LanguageOffer

pub(all) struct LanguageOffer {
tag : String
primary : String
subtags : Array[String]
index : Int
} derive(Eq,
Debug
)

#
LanguageRange

pub(all) struct LanguageRange {
primary : String
subtags : Array[String]
q : Int
order : Int
raw : String
} derive(Eq,
Debug
)

#
MediaCandidate

pub(all) struct MediaCandidate {
offer : MediaOffer
matched_range : MediaRange
quality : Int
specificity : Int
parameter_matches : Int
reason : String
} derive(Eq,
Debug
)

#
MediaDecision

pub(all) struct MediaDecision {
selected : String?
quality : Int
candidate : MediaCandidate?
matches : Array[MediaCandidate]
explanation : String
} derive(Eq,
Debug
)

#
MediaOffer

pub(all) struct MediaOffer {
value : String
top : String
sub : String
params : Array[Param]
index : Int
} derive(Eq,
Debug
)

#
MediaRange

pub(all) struct MediaRange {
top : String
sub : String
params : Array[Param]
extensions : Array[Param]
q : Int
order : Int
raw : String
} derive(Eq,
Debug
)

#
Param

pub(all) struct Param {
name : String
value : String
} derive(Eq,
Debug
)

#
ParseError

pub(all) enum ParseError {
EmptyItem(Int)
MissingSlash(Int, String)
InvalidToken(Int, String, String)
InvalidParameter(Int, String)
InvalidQ(Int, String)
InvalidWildcard(Int, String)
InvalidOffer(Int, String)
InvalidLanguageTag(Int, String)
} derive(Eq,
Debug
)

#
RequestPreferences

pub(all) struct RequestPreferences {
accept : String
accept_language : String
accept_encoding : String
accept_charset : String
} derive(Eq,
Debug
)

#
RequestPreferences::RequestPreferences

fn RequestPreferences::RequestPreferences(accept? : StringView, accept_language? : StringView, accept_encoding? : StringView, accept_charset? : StringView) -> RequestPreferences

#
ResponseNegotiation

pub(all) struct ResponseNegotiation {
decision : VariantDecision
summary : ResponseSummary
headers : Array[Param]
header_block : String
} derive(Eq,
Debug
)

#
ResponseSummary

pub(all) struct ResponseSummary {
status : Int
content_type : String?
content_language : String?
content_encoding : String?
charset : String?
vary : Array[String]
explanation : String
} derive(Eq,
Debug
)

#
ServerVariant

pub(all) struct ServerVariant {
id : String
media : String
language : String?
encoding : String?
charset : String?
server_weight : Int
index : Int
} derive(Eq,
Debug
)

#
ServerVariant::ServerVariant

fn ServerVariant::ServerVariant(id : StringView, media : StringView, language? : StringView, encoding? : StringView, charset? : StringView, server_weight? : Int, index? : Int) -> ServerVariant

#
VariantCandidate

pub(all) struct VariantCandidate {
variant : ServerVariant
media : MediaCandidate?
language : LanguageCandidate?
encoding : EncodingCandidate?
charset : CharsetCandidate?
quality : Int
reason : String
} derive(Eq,
Debug
)

#
VariantDecision

pub(all) struct VariantDecision {
selected : ServerVariant?
quality : Int
matches : Array[VariantCandidate]
vary : Array[String]
explanation : String
} derive(Eq,
Debug
)

#
api_preferences_json_zh_gzip

fn api_preferences_json_zh_gzip() -> RequestPreferences

#
api_response_summary

fn api_response_summary() -> Result[ResponseSummary, ParseError]

#
api_variants

fn api_variants() -> Array[ServerVariant]

#
audit_accept

fn audit_accept(header : StringView) -> HeaderAudit

#
audit_accept_charset

fn audit_accept_charset(header : StringView) -> HeaderAudit

#
audit_accept_encoding

fn audit_accept_encoding(header : StringView) -> HeaderAudit

#
audit_accept_language

fn audit_accept_language(header : StringView) -> HeaderAudit

#
audit_api_scenario

fn audit_api_scenario() -> HeaderAudit

#
audit_browser_scenario

fn audit_browser_scenario() -> HeaderAudit

#
audit_cache_control_header

fn audit_cache_control_header(header : StringView) -> HeaderAudit

#
audit_preferences

fn audit_preferences(preferences : RequestPreferences) -> HeaderAudit

#
audit_vary_header

fn audit_vary_header(header : StringView) -> HeaderAudit

#
browser_preferences_html_en_br

fn browser_preferences_html_en_br() -> RequestPreferences

#
browser_response_summary

fn browser_response_summary() -> Result[ResponseSummary, ParseError]

#
build_response_variants

fn build_response_variants(media_offers : Array[String], language_offers : Array[String], encoding_offers : Array[String], charset_offers : Array[String]) -> Array[ServerVariant]

#
cache_control_has

fn cache_control_has(directives : Array[CacheDirective], name : StringView) -> Bool

#
cache_control_value

fn cache_control_value(directives : Array[CacheDirective], name : StringView) -> String?

#
cache_is_storable

fn cache_is_storable(summary : CachePolicySummary) -> Bool

#
cache_private_fresh_lifetime

fn cache_private_fresh_lifetime(summary : CachePolicySummary) -> Int?

#
cache_requires_revalidation

fn cache_requires_revalidation(summary : CachePolicySummary) -> Bool

#
cache_shared_fresh_lifetime

fn cache_shared_fresh_lifetime(summary : CachePolicySummary) -> Int?

#
canonical_charset

fn canonical_charset(input : StringView) -> String

#
charset_candidate_line

fn charset_candidate_line(candidate : CharsetCandidate) -> String

#
charset_is_acceptable

fn charset_is_acceptable(accept_charset : StringView, charset : StringView) -> Result[Bool, ParseError]

#
conservative_preferences

fn conservative_preferences() -> RequestPreferences

#
decision_vary_header

fn decision_vary_header(decision : VariantDecision) -> String

#
document_variants

fn document_variants() -> Array[ServerVariant]

#
encoding_candidate_line

fn encoding_candidate_line(candidate : EncodingCandidate) -> String

#
encoding_is_acceptable

fn encoding_is_acceptable(accept_encoding : StringView, coding : StringView) -> Result[Bool, ParseError]

#
etag_list_matches

fn etag_list_matches(header_value : StringView, current : EntityTag, strong? : Bool) -> Result[Bool, ParseError]

#
evaluate_if_match

fn evaluate_if_match(header_value : StringView, current : EntityTag) -> Result[ConditionalDecision, ParseError]

#
evaluate_if_none_match

fn evaluate_if_none_match(http_method : StringView, header_value : StringView, current : EntityTag) -> Result[ConditionalDecision, ParseError]

#
explain_charset_candidates

fn explain_charset_candidates(accept_charset : StringView, offers : Array[String]) -> Result[Array[String], ParseError]

#
explain_encoding_candidates

fn explain_encoding_candidates(accept_encoding : StringView, offers : Array[String]) -> Result[Array[String], ParseError]

#
explain_language_candidates

fn explain_language_candidates(accept_language : StringView, offers : Array[String]) -> Result[Array[String], ParseError]

#
explain_media_candidates

fn explain_media_candidates(accept_header : StringView, offers : Array[String]) -> Result[Array[String], ParseError]

#
explain_variant_candidates

fn explain_variant_candidates(decision : VariantDecision) -> Array[String]

#
format_audit

fn format_audit(audit : HeaderAudit) -> String

#
format_cache_control

fn format_cache_control(directives : Array[CacheDirective]) -> String

#
format_cache_summary

fn format_cache_summary(summary : CachePolicySummary) -> String

#
format_charset_decision

fn format_charset_decision(decision : CharsetDecision) -> String

#
format_conditional_decision

fn format_conditional_decision(decision : ConditionalDecision) -> String

#
format_encoding_decision

fn format_encoding_decision(decision : EncodingDecision) -> String

#
format_etag

fn format_etag(tag : EntityTag) -> String

#
format_language_decision

fn format_language_decision(decision : LanguageDecision) -> String

#
format_lines

fn format_lines(lines : Array[String]) -> String

#
format_media_decision

fn format_media_decision(decision : MediaDecision) -> String

#
format_response_headers

fn format_response_headers(headers : Array[Param]) -> String

#
format_response_summary

fn format_response_summary(summary : ResponseSummary) -> String

#
format_variant_decision

fn format_variant_decision(decision : VariantDecision) -> String

#
join_header_names

fn join_header_names(names : Array[String]) -> String

#
language_candidate_line

fn language_candidate_line(candidate : LanguageCandidate) -> String

#
media_candidate_line

fn media_candidate_line(candidate : MediaCandidate) -> String

#
merge_vary

fn merge_vary(left : Array[String], right : Array[String]) -> Result[Array[String], ParseError]

#
negotiate_api_scenario

fn negotiate_api_scenario() -> Result[VariantDecision, ParseError]

#
negotiate_browser_document_scenario

fn negotiate_browser_document_scenario() -> Result[VariantDecision, ParseError]

#
negotiate_charset

fn negotiate_charset(accept_charset : StringView, offers : Array[String]) -> Result[CharsetDecision, ParseError]

#
negotiate_conservative_api_scenario

fn negotiate_conservative_api_scenario() -> Result[VariantDecision, ParseError]

#
negotiate_encoding

fn negotiate_encoding(accept_encoding : StringView, offers : Array[String]) -> Result[EncodingDecision, ParseError]

#
negotiate_language

fn negotiate_language(accept_language : StringView, offers : Array[String]) -> Result[LanguageDecision, ParseError]

#
negotiate_media

fn negotiate_media(accept_header : StringView, offers : Array[String]) -> Result[MediaDecision, ParseError]

#
negotiate_response

fn negotiate_response(preferences : RequestPreferences, media_offers : Array[String], language_offers : Array[String], encoding_offers : Array[String], charset_offers : Array[String]) -> Result[ResponseNegotiation, ParseError]

#
negotiate_response_summary

fn negotiate_response_summary(preferences : RequestPreferences, media_offers : Array[String], language_offers : Array[String], encoding_offers : Array[String], charset_offers : Array[String]) -> Result[ResponseSummary, ParseError]

#
negotiate_variant

fn negotiate_variant(preferences : RequestPreferences, variants : Array[ServerVariant]) -> Result[VariantDecision, ParseError]

#
negotiation_matrix_report

fn negotiation_matrix_report(preferences : RequestPreferences, variants : Array[ServerVariant]) -> Result[String, ParseError]

#
normalize_header_name

fn normalize_header_name(name : StringView) -> Result[String, ParseError]

#
parse_accept

fn parse_accept(header : StringView) -> Result[Array[MediaRange], ParseError]

#
parse_accept_charset

fn parse_accept_charset(header : StringView) -> Result[Array[CharsetRange], ParseError]

#
parse_accept_encoding

fn parse_accept_encoding(header : StringView) -> Result[Array[EncodingRange], ParseError]

#
parse_accept_language

fn parse_accept_language(header : StringView) -> Result[Array[LanguageRange], ParseError]

#
parse_cache_control

fn parse_cache_control(header : StringView) -> Result[Array[CacheDirective], ParseError]

#
parse_cache_policy

fn parse_cache_policy(header : StringView) -> Result[CachePolicySummary, ParseError]

#
parse_charset

fn parse_charset(value : StringView) -> Result[CharsetOffer, ParseError]

#
parse_content_encoding

fn parse_content_encoding(value : StringView) -> Result[EncodingOffer, ParseError]

#
parse_content_type

fn parse_content_type(value : StringView) -> Result[MediaOffer, ParseError]

#
parse_etag

fn parse_etag(value : StringView) -> Result[EntityTag, ParseError]

#
parse_etag_list

fn parse_etag_list(value : StringView) -> Result[Array[EntityTag], ParseError]

#
parse_header_list

fn parse_header_list(value : StringView) -> Result[Array[String], ParseError]

#
parse_language_tag

fn parse_language_tag(tag : StringView) -> Result[LanguageOffer, ParseError]

#
parse_vary

fn parse_vary(value : StringView) -> Result[Array[String], ParseError]

#
quality_to_string

fn quality_to_string(q : Int) -> String

#
response_header_block

fn response_header_block(summary : ResponseSummary) -> String

#
response_header_value

fn response_header_value(summary : ResponseSummary, name : StringView) -> String?

#
response_headers

fn response_headers(summary : ResponseSummary) -> Array[Param]

#
scenario_report

fn scenario_report() -> String

#
strong_etag_match

fn strong_etag_match(left : EntityTag, right : EntityTag) -> Bool

#
summarize_cache_control

fn summarize_cache_control(directives : Array[CacheDirective]) -> Result[CachePolicySummary, ParseError]

#
summarize_response

fn summarize_response(decision : VariantDecision) -> ResponseSummary

#
variant_candidate_line

fn variant_candidate_line(candidate : VariantCandidate) -> String

#
vary_contains

fn vary_contains(vary : Array[String], field : StringView) -> Bool

#
weak_etag_match

fn weak_etag_match(left : EntityTag, right : EntityTag) -> Bool