ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Metabase 的 Clojure 代码风格:命名、Docstring、模块拆分与实用工具速查

Metabase 的 Clojure 代码风格:命名、Docstring、模块拆分与实用工具速查 Metabase 的 Clojure 代码风格命名、Docstring、模块拆分与实用工具速查【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase本篇基于 Metabase 仓库内部的 Clojure 风格指南clojure-style-guide.md系统讲解该项目后端 Clojure / ClojureScript 的编码约定命名规则、docstring 契约写作、命名空间与模块结构、defenterprise的 OSS/EE 分流机制、REST 端点的defendpoint四参数形态、测试编写守则以及metabase.util、Toucan 2、better-cond、Malli 等高频实用工具速查。读完你不仅能按 Metabase 的规范写出可合并的 Clojure 代码还能理解每条规则背后的实现机制例如 clj-kondo 自定义 linter、模块边界 ratchet知道“为什么必须这样写”。命名约定总体规则Metabase 要求所有变量、函数、常量一律使用kebab-case。允许的常见缩写只有七个acc、i、pred、coll、n、s、k、f。函数命名遵循三条原则纯函数应以**描述返回值的 noun名词**命名例如用age而不是calculate-age或get-age有副作用的函数必须以!结尾不要在函数名里重复命名空间别名比如u/log-message就不需要叫util-log-message。解构Destructuringmap 解构的本地绑定一律使用 kebab-case即使 map 的 key 是snake_case——应用数据库的行是唯一的 snake_case 例外转换应在解析边界完成见后文反模式一节。可见性与代码组织一切默认^:private只有确实被其他命名空间使用时才公开组织命名空间时尽量避免declare常用手段是把公开函数放在文件末尾超过 20 行的函数应拆分行宽不超过 120 字符定义形式内部不允许出现“空白的非注释行”let/cond成对结构除外。Java 互操作类型提示 ex-*助手指南给出的标准示例来自原文档;; bad — reflection warning, calls Throwable methods directly (defn root-cause [e] (if-let [cause (.getCause e)] (recur cause) e)) ;; good (defn- root-cause [^Throwable e] (if-let [cause (ex-cause e)] (recur cause) e))要点参数类型提示写在 arglist 里能用ex-cause/ex-message/ex-data/ex-info就不要直接调.getCause/.getMessage/(.getData ^ExceptionInfo ...)。另外每个新的.clj文件在ns形式之后都应写(set! *warn-on-reflection* true).cljc文件除外仓库对这一点是强制的。时间/日期java-time.apiu.date不要直接java.time.*互操作;; bad — 需要 java.time 互操作 (:import (java.time LocalDate)) ... (LocalDate/now) ... ;; good (:require [java-time.api :as t] [metabase.util.date-2 :as u.date]) ... (t/local-date) ... ... (u.date/parse 2024-01-01) ...只有当某个 Java 方法真正需要该 class 时才允许(:import java.time.X)。判断非空用seq不要用count;; bad — count 会把整个懒序列物化进内存 (let [prompts (map build-prompt items)] (when (pos? (count prompts)) (t2/insert! :model/Prompt prompts))) ;; good — seq 只取第一个 cellinsert! 再流式处理其余部分 (let [prompts (map build-prompt items)] (when (seq prompts) (t2/insert! :model/Prompt prompts)))这里的关键是懒序列语义(map f xs)两次执行才会重新计算而已绑定的 seq 会缓存——所以判断非空绝不要用会强制求值的count。关键字与元数据内部使用一律优先带命名空间的关键字:query-type/normal而不是:normal如果一个绑定是函数但没有天然的:arglists元数据应手动打上:arglists元数据不要为metabase.util、metabase.util.malli、metabase.util.log、java-time.api等库发明新的:as别名——使用规范别名如[metabase.util :as u]混用别名会破坏基于 grep 的代码发现能力。Docstring 标准把 docstring 写成“契约”src或enterprise/backend/src下每个公开 var 都必须有 docstring格式遵循 Markdown 惯例引用其他 var 用[[other-var]]而不是反引号。指南对 docstring 的核心定位是docstring 陈述的是契约——它返回什么包括何时返回 nil、参数含义、抛出什么异常 versus 吞掉什么异常、调用方必须遵守的前置条件和不变量。对不显然的、可选的参数写 params 说明块是好事长度本身不是问题。以下七种内容属于“该删或该挪走”的内容原文档以表格形式列出此处完整继承反模式原因应该放哪里你不拥有的事实——被调方、别的命名空间或别的仓库的内部机制会静默漂移过期了也没有任何东西会失败删掉点出被调方名字让它自己的 docstring 去说说了两遍——ns docstring 复述其 var 的、调用方复述被调方的、兄弟函数重复样板两份副本意味着其中一份已经错了删掉副本一个事实一个 owner设计论证——为什么选这个方案、为什么放在这个模块、备选方案是什么调用方无法据此采取行动放 commit message或放在受约束的具体行上的行内注释开发历史——matching master、the X branch、currently、for now、used to天生就是过期的删掉给读者的指令——callers should trust this、dont bother checking、you should use X陈述契约即可调用方自己决定删掉只保留真正的保证作为朴素陈述虚构的精确度——代码里并没有执行的分数区间、阈值、表格读起来像规范其实不是删掉或者让代码真正去执行它写错地方的注释——在 base/默认实现上描述 override 的行为看 override 的人看不到挪到它所描述的 override 上注释规范TODO的固定格式;; TODO (Name YYYY-MM-DD) -- description凡是解释函数体的内容优先写行内注释放在它所解释的那一行上而不是写进 docstring 的散文里。模块结构OSS 与 Enterprise 的命名空间约定OSS 模块命名空间遵循metabase.module.*模式源码放在src/metabase/module/例如 src/metabase/remote_sync/、src/metabase/queries/。Enterprise 模块遵循metabase-enterprise.module.*模式源码放在enterprise/backend/src/metabase_enterprise/module/实际文件用下划线enterprise/backend/src/。模块内部结构REST API 端点放在module.api或module.api.*命名空间模块的公开 API 放在module.core通过 Potemkin 导入shared.ns/import-fns导出Toucan 模型放module.models.*设置放module.settingsschema 放module.schema。模块 linter不得用:clj-kondo/ignore [:metabase/modules]来“骗过”模块边界检查——正确做法是暴露 owning 模块命名空间中的函数再调用它例如跨模块拿数据时禁止用 raw:left-joinSQL 去 join 别的模块的表。defenterpriseOSS/EE 分流的正确姿势指南要求用defenterprise宏而不是 protocol 或运行时 feature 检查。OSS 命名空间定义 fallback 函数体并点名 EE 命名空间EE 命名空间用同名函数覆盖并带一个指向 premium-features 标志的:feature子句。两侧文件使用相同的 docstring 形态——两边的行为都要描述。原文档示例可对照仓库中的 src/metabase/remote_sync/core.clj;; src/metabase/remote_sync/core.clj (OSS) (defenterprise collection-editable? Returns whether remote-synced collections are editable. Always true on OSS; EE consults the remote-sync settings. metabase-enterprise.remote-sync.core [_collection] true) ;; enterprise/backend/src/metabase_enterprise/remote_sync/core.clj (EE) (defenterprise collection-editable? EE override — respects the synced-collections-editable? setting. :feature :remote-sync [collection] (or (synced-collections-editable?) (not (synced? collection))))defenterprise-schema是 malli-schema 版本。两者均从[metabase.premium-features.core :refer [defenterprise defenterprise-schema]]引入。OSS 函数体在 feature flag 关闭时执行——用户代码里永远不要写(when (premium/has-feature? ...) ...)让defenterprise去做路由。源码级印证src/metabase/premium_features/defenterprise.clj 中的dynamic-ee-oss-fn揭示了路由的真实逻辑它返回的函数在每次调用时先惰性、一次性requireEE 命名空间然后按cond分支——(cond (and ee (check-feature feature)) ; EE 代码已加载且 token 当前授予该 feature → EE 实现 (and ee (fn? fallback)) ; 提供了显式 fallback 函数 → fallback :else ; 否则 → OSS 实现 (apply oss args))安全门必须“fail closed”:feature :none技巧这是指南中最有实战价值的一条。从源码结构看dynamic-ee-oss-fn只在“EE 命名空间已加载且当前 token 授予该 feature”两个条件同时满足时才调 EE 实现否则回落到 OSS 实现——也就是fail open失败时宽松。对宽松型默认值如collection-editable?→true这没问题。但对限制性功能沙箱、impersonation、行级过滤OSS fallback 给出的恰恰是“不受限”的答案token 暂时失效/过期时一个被沙箱约束的用户会被静默降级成全权限——fail open安全事故。正确做法当 EE 覆盖实现是“决定是否限制”的判断点时用:feature :noneEE 代码一旦加载就永远路由到 EE 实现并在函数内部显式检查 feature让 token 失效时 failclosed;; enterprise/.../sandbox/api/util.clj — 检测用户是否被沙箱约束的门 (defenterprise enforced-sandboxes-for-user ... ;; :none so we can decide whether sandboxing is *configured* even when the feature ;; isnt currently available — letting us BLOCK requests that would otherwise be ;; sandboxed, instead of returning the permissive OSS fallback. Fail closed. :feature :none [user-id] ...)注意边界这个技巧只用于检测/执行决定点本身下游仅仅执行已有决定的函数继续用正常的:feature :sandboxes门即可。REST API 端点必备要素所有新端点必须有响应 schema路由字符串之后写:- schema所有端点的参数都要有 Malli schema详细且完整所有新 REST API 端点必须有测试——这是硬性要求。端点形态api.macros/defendpoint四个位置参数(api.macros/defendpoint :get /:key/callback OIDC callback for a specific provider. [{provider-key :key} :- [:map [:key ProviderKey]] _query-params _body request] (oidc-integration/sso-callback provider-key request))arglist 始终是四个位置绑定顺序固定route-params、query-params、body、request。未使用的参数用_前缀命名。每个参数都可以用:- schema标注 Malli schema_开头的可省略。HTTP 动词是关键字:get/:post/:put/:delete不是 symbol。需要从[metabase.api.macros :as api.macros]引入该宏——仓库中该宏定义在 src/metabase/api/macros.clj。命名与行为约定查询参数用 kebab-case请求体用snake_case路由用单数名词如/api/dashboard/:idGET端点不应有副作用analytics 事件除外defendpoint形式应是小而薄的 wrapper业务逻辑放在 Toucan 模型代码里。封闭响应 map{:closed true}响应 Malli:mapschema 要打上{:closed true}这样生成的 OpenAPI / TypeScript 类型会拒绝多余 key。开放 map 会产出additionalProperties: true导致前端类型变弱、discriminated union 失效。同一轮修改里还应收紧领域类型——能写pos-int?就不要写:int当别处不存在为零的分支时。;; weak {:status 200 :body [:map [:status [: :generated]] [:prompt_count :int]]} ;; tight {:status 200 :body [:map {:closed true} [:status [: :generated]] [:prompt_count pos-int?]]}测试编写守则组织大测试按逻辑用例拆成独立的deftest测试名以-test或-test-number结尾。性能纯函数测试标记^:parallel。打桩defnvar用mt/with-dynamic-fn-redefs而不是with-redefs打桩defn/defn-var 时裸with-redefs在并行测试下不安全且有一条 clj-kondo hook:metabase/prefer-with-dynamic-fn-redefs配置见 .clj-kondo/config.edn会告警with-redefs仍然适用于def/动态 var 重绑定。;; bad — 被 linter 标记:parallel 测试下不安全 (with-redefs [my.ns/some-fn (fn [_] :stub)] (do-test)) ;; good (mt/with-dynamic-fn-redefs [my.ns/some-fn (fn [_] :stub)] (do-test))这条规则在仓库里有真实的执行机制自定义 linter 实现在 .clj-kondo/src/hooks/clojure/core/with_redefs.clj其自测用例在 .clj-kondo/test/hooks/clojure/core/with_redefs_test.clj.clj-kondo/ratchets.edn 中对存量违规做了计数锁ratchet只减不增。异步测试禁止裸Thread/sleep用带超时哨兵的deref固定 sleep 是 CI flake 的第一来源。测试应当在契约被违反时快速、大声地失败而不是挂起或“睡过”bug;; bad — 慢 CI 上 flaky真正失败时静默 (Thread/sleep 300) (is ( :expected seen)) ;; good — 快速失败并给出清晰信息 (let [v (deref some-promise 1000 ::timeout)] (is (not ::timeout v) promise never delivered) (is ( :expected v)))MBQLMetabase Query Language限制在lib、lib-be、query-processor模块之外禁止对原始 MBQL 做内省操作新代码使用 LibMBQL 5避免 legacy MBQL。数据库与模型命名模型名与表名用单数名词应用数据库app db标识符用snake_case。最佳实践只需要一列时用t2/select-one-fn不要取整行把正确行为放进 Toucan 方法method而不是另起 helper 函数。数据库驱动Driver约定文档新增 driver 多方法必须在 docs/developers-guide/driver-changelog.md 中提及。实现driver 实现调用其他 driver 多方法时应把driver参数传递下去不要在实现中硬编码 driver 名字基于 JDBC 的 driver 中read-column-thunk内部的逻辑要尽量小。实用工具速查Helper Quick ReferenceMetabase 及其依赖库提供了大量让代码更可读的 helper 函数。指南反复强调不要手写已经存在的东西第一次就该用对 helper作者优先右列reviewer 应在 PR 中标记左列。“我写了 X应该用 Y”对照表你写了应该用什么时候不适用(vec (keep f xs))(u/keepv f xs)—(let [x (...)] (side-effect!) x)(u/prog1 (...) (side-effect!))—是 anaphor绑定结果(into {} (map (juxt k identity)) xs)(u/index-by k xs)—(into {} (map (juxt k vf)) xs)(u/index-by k vf xs)—#(compare %2 %1)u/reverse-compare—(:col (t2/select-one [Model :col] …))(t2/select-one-fn :col Model …)—(or (:k m) default)(:k m default)值可能“存在但为 nil”如 DB 中的 null 水合值(let [x (...)] (if x then else))(if-let [x (...)] then else)—(filter #( :a (:type %)) coll) 对:b再来一遍…(group-by :type coll)再分发—(- xs (map f) (remove g) (mapv h))(into [] (comp (map f) (remove g) (map h)) xs)管道线程可读性更高时嵌套let/if/if-letb/cond配:let两层嵌套保持原样可能更清晰函数体第一个形式(let [{:keys [k]} arg] …)直接在 arglist 里解构解构还引用了let里计算的东西时每个字段都(or (:k m) (get m (name k)))归一化一次(update-keys m keyword)之后直接用:k确实需要逐字段处理两种形态时metabase.util惯例别名[metabase.util :as u]序列/集合(u/keepv f coll)——keep但返回向量立即求值(u/index-by kf coll)/(u/index-by kf vf coll)——生成{(kf x) (vf x)}map。字符串locale 安全(u/lower-case-en s)、(u/upper-case-en s)——结果进入匹配/比较时必须用这两个而不是clojure.string/lower-case/upper-caselocale 默认版本在土耳其语i上会出问题(u/qualified-name x)——keyword →ns/name或namesymbol →(name x)string → 原样。控制流(u/prog1 first-form body)——anaphoricfirst-form的结果绑定到返回first-form的值。用途“做一件事用其结果做副作用然后返回那件事”(u/the-id x)——从 int 或实例取实体 id。比较器(u/reverse-compare x y)——即(compare y x)作为排序比较器传入(sort-by f u/reverse-compare coll)。其他(u/conflicting-keys m1 m2)——两边都有但值不同的 key。metabase.util.cljc全文约 1300 行仓库中为 src/metabase/util.cljc——拿不准时 grep 它。metabase.util.namespaces惯例别名shared.ns(shared.ns/import-fns [other-ns f1 f2] [another-ns f3])——把别的命名空间的 defn 通过当前命名空间再导出。:require [other-ns :as other-ns]别名必须保留——clj-kondo 可能标记它为未使用但宏展开时确实需要。加一条;; needed for import-fns注释防止它被自动删除。Toucan 2惯例别名[toucan2.core :as t2]单字段读取避免取整行(t2/select-one-fn :col Model :id 123)——投影友好f是 keyword 时只 fetch 该列用它替代(:col (t2/select-one [Model :col] …))(t2/select-fn-set f Model conditions)——(f row)值的集合(t2/select-fn-vec f Model conditions)——(f row)值的向量(t2/select-pks-set Model conditions)——主键集合。存在性/计数(t2/exists? Model conditions)→ 布尔(t2/count Model conditions)→ long。写入(t2/insert-returning-instances! Model row-or-rows)——返回带主键的行(t2/insert-returning-pks! Model row-or-rows)——只返回主键(t2/update! Model id-or-conditions changes)(t2/delete! Model id-or-conditions)。事务(t2/with-transaction [t-conn] body…)——在 app db 连接上开事务。水合Hydration(t2/hydrate instances :key1 :key2)——触发:batched-hydrate方法用(methodical/defmethod t2/batched-hydrate [:model/Foo :the-key] [_ k rows] …)定义。定义模型(methodical/defmethod t2/table-name :model/Foo [_] :foo_table)(t2/deftransforms :model/Foo {:col mi/transform-json})——常用转换transform-json、transform-secret-value加密、transform-keyword。better-cond[better-cond.core :as b](b/cond …)类似cond但支持穿插:let [bindings]与:when子句能把级联的let/if/if-let/let拍平成一条线性主干(b/cond :let [thread (load-thread id)] (nil? thread) :not-found :let [exploration (load-exploration (:exploration-id thread)) reason (gate-check exploration)] reason (skip! reason) :let [pool (build-pool thread)] (empty? pool) :skip-no-data :else (run! thread pool))当函数否则会是一串阶梯式嵌套时才用它单个if/let不需要。Schema共享的用mr/def单处的写内联schema 在 2 处以上使用、或名字值得被文档化时用mr/def提进注册表(:require [metabase.util.malli.registry :as mr]) (mr/def ::options Options for Pulse channel rendering. [:map [:channel.render/include-buttons? {:description default: false :optional true} :boolean]])mr/def的 key 永远是::namespacedkeyword下游调用方用完全限定 keyword 引用。单处使用的 schema 留在mu/defn的:-注解里不必提取。Mallimetabase.util.malli(mu/defn my-fn :- :string [x :- :int, y :- [:maybe :string]] …)——内联 schema默认 dev 下做运行时校验、prod 下为 no-op(mu/disable-enforcement body)——包住测试/性能敏感的代码块常用 malli schema:int、:string、:boolean、:keyword、:map、[:maybe T]、[:sequential T]、[:enum :a :b]、[:map [:k T]]、pos-int?、nat-int?。指南的反模式清单进一步强调函数只要有真实契约就用mu/defn或mu/defn-并对返回和每个参数标注:- schema——这是本仓库默认写法裸defn只留给琐碎的 dev/helper 代码。i18n[metabase.util.i18n :refer [tru deferred-tru]](tru Hello {0} name)——调用时翻译用于请求处理代码(deferred-tru …)——读取时翻译用于静态上下文defsetting :description、defn元数据的:description、顶层def的用户可见 labelICU MessageFormat字面量撇号用双撇号(tru its ready)数字占位符{0}、{1}开发者专用的调试字符串REPL 帮助文本、内部日志不要翻译。日志[metabase.util.log :as log]级别trace、debug、info、warn、error每级两种形态(log/warn msg)或(log/warn e msg)——有异常时异常在前(log/warnf fmt %s arg)或(log/warnf e fmt %s arg)——printf 风格不要写(log/warn (format fmt %s arg))——直接用warnf。Map / key 归一化解析松散类型的外部数据LLM JSON、请求体且 key 可能是 string 或 keyword 时(update-keys m keyword)——顶层 key 转 keywordClojure 1.11clojure.core内置(clojure.walk/keywordize-keys m)——递归慎用嵌套 map 里的值字符串可能被过度 keyword 化归一化一次之后下游代码就用朴素的(:k m)——不要在每个使用点写(or (:k m) (get m (name k)))。其他 map 操作(update-vals m f)——Clojure 1.11clojure.core内置(medley.core/map-vals f m)、(m/map-keys f m)——旧替代。本仓库Medley 锁定在 1.4m/index-of等较新的函数不可用。比较器与排序(sort-by f u/reverse-compare coll)——降序向量比较器(sort-by (juxt :primary :secondary) coll)主数值 key 取负实现反转(sort-by (juxt #(- (:score %)) :id) coll)平局时按:id升序。反模式清单指南最后汇总的“避免项”每条都对应上面某一节的正面规则完整继承(let [x (do-thing!)] (other-thing! x) x)——用u/prog1(vec (keep …))、(vec (map …))——用u/keepv或 transducer(into [] (comp …) coll)对同一 coll 多次filter不同:type——一次(group-by :type coll)逐字段 string/keyword 双查——用update-keys keyword归一化一次用 raw:left-joinSQL 伸进别的模块的表——在 owning 模型命名空间暴露函数并调用它对内存中加工后的 map 用 snake_case key——那是 appdb 行专属的在解析边界如 json 响应转内部 record 时转换对水合的 DB 列写(double (:k m 0.0))——“存在但为 nil”会 NPE写(double (or (:k m) 0.0))一整页放所有 impl 的defmulti——扩展性故事只是虚构用私有查表 map 私有函数对懒序列(pos? (count xs))判非空——count强制物化整个 seq用(seq xs)测试中对defnvar 用裸with-redefs——非并行安全kondo hook 会告警用mt/with-dynamic-fn-redefs异步测试中(Thread/sleep N)——flake 第一来源(deref p timeout-ms ::timeout)并断言not ::timeout手写(str/split host #:)比较 origin/host——处理不好 IPv6用mw.security/parse-url客户端可控输入用try-parse-url;日志路径上(subs (pr-str huge) 0 N)——多 MB 字符串已经分配了改为在pr-str周围绑定*print-length*/*print-level*新写 retry 循环 / 新写“找这一列”的 helper / 新的 transform-source-type 谓词——先搜索。metabase.util.retry/with-retry、metabase.lib.equality/find-matching-column、metabase.transforms.feature-gating/any-transforms-enabled?已经存在还有很多。Reviewer 最常问的就是“这个是不是已经有现成的 X 了”有契约的函数用裸defn——mu/defn:- schema是默认新代码里(:import (java.time …))(LocalDate/now)——用[java-time.api :as t][metabase.util.date-2 :as u.date]裸defmethod——用methodical/defmethod它支持 docstring全库统一使用代码里(when (premium/has-feature? :flag) …)——用defenterprise分流OSS/EE 函数体各自住在对应命名空间不要到处撒运行时 feature 检查新.clj文件在ns形式后没有(set! *warn-on-reflection* true)——本仓库每个.clj源文件都有.cljc除外(:require [a :as a] [b :as b])挤一行(:import java.io.File java.net.URI)裸写——形态都错:require下每条一行:import用(Package Class)列表为metabase.util、metabase.util.malli、metabase.util.log、java-time.api等发明新的:as别名——用规范别名混用别名会让基于 grep 的发现更差。Linter 抑制的正确形态正确的形态是一个reader-discard map直接放在被静默的形式上方显式列出规则关键字并附必需的解释;; Loaded by name from the plugin registry. #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn plugin-entrypoint ...)对单个 require 条目挂在条目上的元数据里(:require ^{:clj-kondo/ignore [:deprecated-namespace :discouraged-namespace]} [metabase.query-processor.store :as qp.store])要点用 map 形式不要用 keyword 形式#_:clj-kondo/ignore否则搜索无法识别被抑制的规则:clj-kondo/ignore保持为 map 的第一个 key——ratchet 扫描器会拒绝其他顺序能修警告就修警告加抑制是最后手段且需要审批。必要时在其正上方或同一行末尾加解释性;;注释。CI 用./bin/mage kondo-ratchets强制这些要求ratchet 机制见 .clj-kondo/ratchets.edn。其他杂项约定可配置项不要定义只能靠环境变量设置的 configurable option——改用:internal的defsetting示例数据示例数据尽可能用鸟类主题bird-themed拿不准时grep -n defn src/metabase/util.cljc | head -80扫一遍 helpergrep -rn defmacro\|defn-? [a-z-]*\?\b src/metabase/util.cljc | head找谓词看起来“像集合操作”就先查medley.core/——但记住 Medley 锁定在 1.4在代码库里搜你要写的模式是否已有用法——如果没人这么写通常说明存在一个 helper。小结Metabase 的 Clojure 风格指南本质上是一套“可执行”的规范命名与结构约定保证了跨模块的一致性metabase.module.*/metabase-enterprise.module.*module.core/api/models/settings/schema分层docstring 契约 七类反模式表格把“注释写什么”变成了可审查标准defenterprisedynamic-ee-oss-fnsrc/metabase/premium_features/defenterprise.clj从实现上杜绝了运行时 feature 检查并用:feature :none技巧让安全门 fail closeddefendpoint四参数形态 {:closed true}响应 schema 直接决定 OpenAPI/TypeScript 生成质量而 helper 速查表u/keepv、u/prog1、t2/select-one-fn、b/cond、mr/def等把“不要重复造轮子”落实成了可对照的具体函数。配合 clj-kondo 自定义 linter如:metabase/prefer-with-dynamic-fn-redefs与kondo-ratchetsCI 检查这套规范不是纸面的而是被工具链持续执行的。【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表